/*
 * Copyright (c) 2018, Nordic Semiconductor
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package no.nordicsemi.android.ble;

import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.UUID;

import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import no.nordicsemi.android.ble.annotation.ConnectionPriority;
import no.nordicsemi.android.ble.annotation.ConnectionState;
import no.nordicsemi.android.ble.annotation.PhyMask;
import no.nordicsemi.android.ble.annotation.PhyOption;
import no.nordicsemi.android.ble.annotation.PhyValue;
import no.nordicsemi.android.ble.callback.ConnectionPriorityCallback;
import no.nordicsemi.android.ble.callback.DataReceivedCallback;
import no.nordicsemi.android.ble.callback.FailCallback;
import no.nordicsemi.android.ble.callback.MtuCallback;
import no.nordicsemi.android.ble.callback.SuccessCallback;
import no.nordicsemi.android.ble.data.Data;
import no.nordicsemi.android.ble.data.DataMerger;
import no.nordicsemi.android.ble.data.DataSplitter;
import no.nordicsemi.android.ble.error.GattError;
import no.nordicsemi.android.ble.utils.ILogger;
import no.nordicsemi.android.ble.utils.ParserUtils;

// DO NOT EDIT THIS FILE UNLESS NECESSARY!

/**
 * <p>
 * The BleManager is responsible for managing the low level communication with a Bluetooth LE device.
 * Please see profiles implementation in Android nRF Blinky or Android nRF Toolbox app for an
 * example of use.
 * <p>
 * This base manager has been tested against number of devices and samples from Nordic SDK.
 * <p>
 * The manager handles connection events and initializes the device after establishing the connection.
 * <ol>
 * <li>For bonded devices it ensures that the Service Changed indications, if this characteristic
 * is present, are enabled. Before Android Marshmallow, Android did not enable them by default,
 * leaving this to the developers.</li>
 * <li><strike>The manager tries to read the Battery Level characteristic. No matter the result of
 * this operation (for example the Battery Level characteristic may not have the READ property)
 * it tries to enable Battery Level notifications to get battery updates from the device.</strike>
 * This feature is now deprecated and will not work with the new API. Instead, read or enabledBattery Level
 * notifications just like any other.</li>
 * <li>After connecting and service discovery, the manager initializes the device using given queue
 * of commands. See {@link BleManagerGattCallback#initialize()} method for more details.</li>
 * <li>When initialization complete, the {@link BleManagerCallbacks#onDeviceReady(BluetoothDevice)}
 * callback is called.</li>
 * </ol>
 * <p>
 * <strike>The manager also is responsible for parsing the Battery Level values and calling
 * {@link BleManagerCallbacks#onBatteryValueReceived(BluetoothDevice, int)} method.</strike>
 * <p>
 * To get logs, override the {@link #log(int, String)} method.
 * <p>
 * The BleManager should be overridden in your app and all the 'high level' callbacks should
 * be called from there. Keeping this file as is (and {@link BleManagerCallbacks} as well)
 * will allow to quickly update it when an update is posted here.
 *
 * @param <E> The profile callbacks type.
 */
@SuppressWarnings({"WeakerAccess", "unused", "DeprecatedIsStillUsed", "deprecation"})
public abstract class BleManager<E extends BleManagerCallbacks> extends TimeoutHandler implements ILogger {
	private final static String TAG = "BleManager";

	private final static UUID CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");

	private final static UUID BATTERY_SERVICE = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb");
	private final static UUID BATTERY_LEVEL_CHARACTERISTIC = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb");

	private final static UUID GENERIC_ATTRIBUTE_SERVICE = UUID.fromString("00001801-0000-1000-8000-00805f9b34fb");
	private final static UUID SERVICE_CHANGED_CHARACTERISTIC = UUID.fromString("00002A05-0000-1000-8000-00805f9b34fb");

	private final Object mLock = new Object();
	private final Context mContext;
	final Handler mHandler;

	private BluetoothGatt mBluetoothGatt;
	private BluetoothDevice mBluetoothDevice;
	private BleManagerGattCallback mGattCallback;
	protected E mCallbacks;
	/**
	 * This flag is set to false only when the {@link ConnectRequest#shouldAutoConnect()} method
	 * returns true and the device got disconnected without calling {@link #disconnect()} method.
	 * If {@link ConnectRequest#shouldAutoConnect()} returns false (default) this is always
	 * set to true.
	 */
	private boolean mUserDisconnected;
	/**
	 * Flag set to true when {@link ConnectRequest#shouldAutoConnect()} method returned true.
	 * The first connection attempt is done with <code>autoConnect</code> flag set to false
	 * (to make the first connection quick) but on connection lost the manager will call
	 * {@link #connect(BluetoothDevice)} again. This time this method will call
	 * {@link BluetoothGatt#connect()} which always uses <code>autoConnect</code> equal true.
	 */
	private boolean mInitialConnection;
	/**
	 * Flag set to true when the device is connected.
	 */
	private boolean mConnected;
	/**
	 * A timestamp when the last connection attempt was made. This is distinguish two situations
	 * when the 133 error happens during a connection attempt: a timeout (when ~30 sec passed since
	 * connection was requested), or an error (packet collision, packet missed, etc.)
	 */
	private long mConnectionTime;
	/**
	 * A time after which receiving 133 error is considered a timeout, instead of a
	 * different reason.
	 * A {@link BluetoothDevice#connectGatt(Context, boolean, BluetoothGattCallback)} call will
	 * fail after 30 seconds if the device won't be found until then. Other errors happen much
	 * earlier. 20 sec should be OK here.
	 */
	private final static long CONNECTION_TIMEOUT_THRESHOLD = 20000; // ms
	/**
	 * Flag set to true when the initialization queue is complete.
	 */
	private boolean mReady;
	/**
	 * Flag set when services were discovered.
	 */
	private boolean mServicesDiscovered;
	private boolean mServiceDiscoveryRequested;
	private int mConnectionCount = 0;
	/**
	 * Connection state. One of {@link BluetoothGatt#STATE_CONNECTED},
	 * {@link BluetoothGatt#STATE_CONNECTING}, {@link BluetoothGatt#STATE_DISCONNECTED},
	 * {@link BluetoothGatt#STATE_DISCONNECTING}.
	 */
	private int mConnectionState = BluetoothGatt.STATE_DISCONNECTED;
	/**
	 * Last received battery value or -1 if value wasn't received.
	 *
	 * @deprecated Battery value should be kept in the profile manager instead. See BatteryManager
	 * class in Android nRF Toolbox app.
	 */
	@IntRange(from = -1, to = 100)
	@Deprecated
	private int mBatteryValue = -1;
	/**
	 * The current MTU (Maximum Transfer Unit). The maximum number of bytes that can be sent in
	 * a single packet is MTU-3.
	 */
	private int mMtu = 23;
	/** A flag indicating that Reliable Write is in progress. */
	private boolean mReliableWriteInProgress;
	/**
	 * The connect request. This is instantiated in {@link #connect(BluetoothDevice, int)}
	 * and nullified after the device is ready.
	 * <p>
	 * This request has a separate reference, as it is notified when the device becomes ready,
	 * after the initialization requests are done.
	 */
	private ConnectRequest mConnectRequest;
	/**
	 * Currently performed request or null in idle state.
	 */
	private Request mRequest;
	/**
	 * Currently performer request set, or null if none.
	 */
	private RequestQueue mRequestQueue;
	/**
	 * A map of {@link ValueChangedCallback}s for handling notifications and indications.
	 */
	private final HashMap<BluetoothGattCharacteristic, ValueChangedCallback> mNotificationCallbacks = new HashMap<>();
	/**
	 * An instance of a request that waits for a notification or an indication.
	 * There may be only a single instance of such request at a time as this is a blocking request.
	 */
	private WaitForValueChangedRequest mValueChangedRequest;
	@Deprecated
	private ValueChangedCallback mBatteryLevelNotificationCallback;

	private final BroadcastReceiver mBluetoothStateBroadcastReceiver = new BroadcastReceiver() {
		@Override
		public void onReceive(final Context context, final Intent intent) {
			final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
			final int previousState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, BluetoothAdapter.STATE_OFF);

			final String stateString = "[Broadcast] Action received: " + BluetoothAdapter.ACTION_STATE_CHANGED +
					", state changed to " + state2String(state);
			log(Log.DEBUG, stateString);

			switch (state) {
				case BluetoothAdapter.STATE_TURNING_OFF:
				case BluetoothAdapter.STATE_OFF:
					if (previousState != BluetoothAdapter.STATE_TURNING_OFF
							&& previousState != BluetoothAdapter.STATE_OFF) {
						final BleManagerGattCallback callback = mGattCallback;
						if (callback != null) {
							// No more calls are possible
							callback.mOperationInProgress = true;
							callback.cancelQueue();
							callback.mInitQueue = null;
						}

						final BluetoothDevice device = mBluetoothDevice;
						if (device != null) {
							// Signal the current request, if any
							if (mRequest != null && mRequest.type != Request.Type.DISCONNECT) {
								mRequest.notifyFail(device, FailCallback.REASON_BLUETOOTH_DISABLED);
								mRequest = null;
							}
							if (mValueChangedRequest != null) {
								mValueChangedRequest.notifyFail(device, FailCallback.REASON_BLUETOOTH_DISABLED);
								mValueChangedRequest = null;
							}
							if (mConnectRequest != null) {
								mConnectRequest.notifyFail(device, FailCallback.REASON_BLUETOOTH_DISABLED);
								mConnectRequest = null;
							}
						}

						// The connection is killed by the system, no need to disconnect gently.
						mUserDisconnected = true;
						if (callback != null) {
							// Allow new requests when Bluetooth is enabled again. close() doesn't do it.
							// See: https://github.com/NordicSemiconductor/Android-BLE-Library/issues/25
							// and: https://github.com/NordicSemiconductor/Android-BLE-Library/issues/41
							callback.mOperationInProgress = false;
							// This will call close()
							if (device != null) {
								callback.notifyDeviceDisconnected(device);
							}
						}
					} else {
						// Calling close() will prevent the STATE_OFF event from being logged
						// (this receiver will be unregistered). But it doesn't matter.
						close();
					}
					break;
			}
		}

		private String state2String(final int state) {
			switch (state) {
				case BluetoothAdapter.STATE_TURNING_ON:
					return "TURNING ON";
				case BluetoothAdapter.STATE_ON:
					return "ON";
				case BluetoothAdapter.STATE_TURNING_OFF:
					return "TURNING OFF";
				case BluetoothAdapter.STATE_OFF:
					return "OFF";
				default:
					return "UNKNOWN (" + state + ")";
			}
		}
	};

	private BroadcastReceiver mBondingBroadcastReceiver = new BroadcastReceiver() {
		@Override
		public void onReceive(final Context context, final Intent intent) {
			final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
			final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
			final int previousBondState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1);

			// Skip other devices.
			if (mBluetoothDevice == null || !device.getAddress().equals(mBluetoothDevice.getAddress()))
				return;

			log(Log.DEBUG, "[Broadcast] Action received: " + BluetoothDevice.ACTION_BOND_STATE_CHANGED +
					", bond state changed to: " + bondStateToString(bondState) + " (" + bondState + ")");

			switch (bondState) {
				case BluetoothDevice.BOND_NONE:
					if (previousBondState == BluetoothDevice.BOND_BONDING) {
						mCallbacks.onBondingFailed(device);
						log(Log.WARN, "Bonding failed");
						if (mRequest != null) { // CREATE_BOND request
							mRequest.notifyFail(device, FailCallback.REASON_REQUEST_FAILED);
							mRequest = null;
						}
					} else if (previousBondState == BluetoothDevice.BOND_BONDED) {
						if (mRequest != null && mRequest.type == Request.Type.REMOVE_BOND) {
							// The device has already disconnected by now.
							log(Log.INFO, "Bond information removed");
							mRequest.notifySuccess(device);
							mRequest = null;
						}
					}
					break;
				case BluetoothDevice.BOND_BONDING:
					mCallbacks.onBondingRequired(device);
					return;
				case BluetoothDevice.BOND_BONDED:
					log(Log.INFO, "Device bonded");
					mCallbacks.onBonded(device);
					if (mRequest != null && mRequest.type == Request.Type.CREATE_BOND) {
						mRequest.notifySuccess(device);
						mRequest = null;
						break;
					}
					// If the device started to pair just after the connection was
					// established the services were not discovered.
					if (!mServicesDiscovered && !mServiceDiscoveryRequested) {
						mServiceDiscoveryRequested = true;
						mHandler.post(() -> {
							log(Log.VERBOSE, "Discovering services...");
							log(Log.DEBUG, "gatt.discoverServices()");
							mBluetoothGatt.discoverServices();
						});
						return;
					}
					// On older Android versions, after executing a command on secured attribute
					// of a device that is not bonded, let's say a write characteristic operation,
					// the system will start bonding. The BOND_BONDING and BOND_BONDED events will
					// be received, but the command will not be repeated automatically.
					//
					// Test results:
					// Devices that require repeating the last task:
					// - Nexus 4 with Android 5.1.1
					// - Samsung S6 with 5.0.1
					// - Samsung S8 with Android 7.0
					// - Nexus 9 with Android 7.1.1
					// Devices that repeat the request automatically:
					// - Pixel 2 with Android 8.1.0
					// - Samsung S8 with Android 8.0.0
					//
					if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
						if (mRequest != null && mRequest.type != Request.Type.CREATE_BOND) {
							// Repeat the last command in that case.
							mGattCallback.enqueueFirst(mRequest);
							break;
						}
					}
					// No need to repeat the request.
					return;
			}
			mGattCallback.nextRequest(true);
		}
	};

	private final BroadcastReceiver mPairingRequestBroadcastReceiver = new BroadcastReceiver() {
		@Override
		public void onReceive(final Context context, final Intent intent) {
			final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

			// Skip other devices.
			if (mBluetoothDevice == null || device == null
					|| !device.getAddress().equals(mBluetoothDevice.getAddress()))
				return;

			// String values are used as the constants are not available for Android 4.3.
			final int variant = intent.getIntExtra("android.bluetooth.device.extra.PAIRING_VARIANT"/*BluetoothDevice.EXTRA_PAIRING_VARIANT*/, 0);
			log(Log.DEBUG, "[Broadcast] Action received: android.bluetooth.device.action.PAIRING_REQUEST"/*BluetoothDevice.ACTION_PAIRING_REQUEST*/ +
					", pairing variant: " + pairingVariantToString(variant) + " (" + variant + ")");

			onPairingRequestReceived(device, variant);
		}
	};

	/**
	 * This method will be called if a remote device requires a non-'just works' pairing.
	 * See PAIRING_* constants for possible options.
	 *
	 * @param device  the device.
	 * @param variant pairing variant.
	 */
	protected void onPairingRequestReceived(@NonNull final BluetoothDevice device,
											@PairingVariant final int variant) {
		// The API below is available for Android 4.4 or newer.

		// An app may set the PIN here or set pairing confirmation (depending on the variant) using:
		// device.setPin(new byte[] { '1', '2', '3', '4', '5', '6' });
		// device.setPairingConfirmation(true);

		// However, setting the PIN here will not prevent from displaying the default pairing
		// dialog, which is shown by another application (Bluetooth Settings).
	}

	/**
	 * The manager constructor.
	 * <p>
	 * After constructing the manager, the callbacks object must be set with
	 * {@link #setGattCallbacks(BleManagerCallbacks)}.
	 * <p>
	 * To connect a device, call {@link #connect(BluetoothDevice)}.
	 *
	 * @param context the context.
	 */
	public BleManager(@NonNull final Context context) {
		mContext = context.getApplicationContext();
		mHandler = new Handler(Looper.getMainLooper());
	}

	/**
	 * Sets the manager callback listener.
	 *
	 * @param callbacks the callback listener.
	 */
	public void setGattCallbacks(@NonNull final E callbacks) {
		mCallbacks = callbacks;
	}

	/**
	 * This method must return the gatt callback used by the manager.
	 * This method must not create a new gatt callback each time it is being invoked, but rather
	 * return a single object.
	 *
	 * @return The gatt callback object.
	 */
	@NonNull
	protected abstract BleManagerGattCallback getGattCallback();

	/**
	 * Returns the context that the manager was created with.
	 *
	 * @return The context.
	 */
	@NonNull
	protected final Context getContext() {
		return mContext;
	}

	/**
	 * Returns the Bluetooth device object used in {@link #connect(BluetoothDevice)}.
	 *
	 * @return The Bluetooth device or null, if {@link #connect(BluetoothDevice)} wasn't called.
	 */
	@Nullable
	// This method is not final, as some Managers may be created with BluetoothDevice in a
	// constructor. Those can return the device object even without calling connect(device).
	public BluetoothDevice getBluetoothDevice() {
		return mBluetoothDevice;
	}

	/**
	 * This method returns true if the device is connected. Services could have not been
	 * discovered yet.
	 */
	public final boolean isConnected() {
		return mConnected;
	}

	/**
	 * Returns whether the target device is bonded. The device does not have to be connected,
	 * but must have been set prior to call this method.
	 *
	 * @return True, if the Android has bonds information of the device. This does not mean that
	 * the target device also has such information, or that the link is in fact encrypted.
	 */
	protected final boolean isBonded() {
		return mBluetoothDevice != null
				&& mBluetoothDevice.getBondState() == BluetoothDevice.BOND_BONDED;
	}

	/**
	 * Returns true if the device is connected and the initialization has finished,
	 * that is when {@link BleManagerGattCallback#onDeviceReady()} was called.
	 */
	public final boolean isReady() {
		return mReady;
	}

	/**
	 * Method returns the connection state:
	 * {@link BluetoothProfile#STATE_CONNECTING STATE_CONNECTING},
	 * {@link BluetoothProfile#STATE_CONNECTED STATE_CONNECTED},
	 * {@link BluetoothProfile#STATE_DISCONNECTING STATE_DISCONNECTING},
	 * {@link BluetoothProfile#STATE_DISCONNECTED STATE_DISCONNECTED}
	 *
	 * @return The connection state.
	 */
	@ConnectionState
	public final int getConnectionState() {
		return mConnectionState;
	}

	/**
	 * Returns the last received value of Battery Level characteristic, or -1 if such
	 * does not exist, hasn't been read or notification wasn't received yet.
	 * <p>
	 * The value returned will be invalid if overridden {@link #readBatteryLevel()} and
	 * {@link #enableBatteryLevelNotifications()} were used.
	 *
	 * @return The last battery level value in percent.
	 * @deprecated Keep the battery level in your manager instead.
	 */
	@IntRange(from = -1, to = 100)
	@Deprecated
	public final int getBatteryValue() {
		return mBatteryValue;
	}

	@Override
	public void log(final int priority, @NonNull final String message) {
		// Override to log events. Simple log can use Logcat:
		//
		// Log.println(priority, TAG, message);
		//
		// You may also use Timber:
		//
		// Timber.log(priority, message);
		//
		// or nRF Logger:
		//
		// Logger.log(logSession, LogContract.Log.Level.fromPriority(priority), message);
		//
		// Starting from nRF Logger 2.1.3, you may use log-timber and plant nRFLoggerTree.
		// https://github.com/NordicSemiconductor/nRF-Logger-API
	}

	@Override
	public void log(final int priority, @StringRes final int messageRes,
					@Nullable final Object... params) {
		final String message = mContext.getString(messageRes, params);
		log(priority, message);
	}

	/**
	 * Returns whether to connect to the remote device just once (false) or to add the address to
	 * white list of devices that will be automatically connect as soon as they become available
	 * (true). In the latter case, if Bluetooth adapter is enabled, Android scans periodically
	 * for devices from the white list and if a advertising packet is received from such, it tries
	 * to connect to it. When the connection is lost, the system will keep trying to reconnect to
	 * it in. If true is returned, and the connection to the device is lost the
	 * {@link BleManagerCallbacks#onLinkLossOccurred(BluetoothDevice)} callback is called instead of
	 * {@link BleManagerCallbacks#onDeviceDisconnected(BluetoothDevice)}.
	 * <p>
	 * This feature works much better on newer Android phone models and many not work on older
	 * phones.
	 * <p>
	 * This method should only be used with bonded devices, as otherwise the device may change
	 * it's address. It will however work also with non-bonded devices with private static address.
	 * A connection attempt to a device with private resolvable address will fail.
	 * <p>
	 * The first connection to a device will always be created with autoConnect flag to false
	 * (see {@link BluetoothDevice#connectGatt(Context, boolean, BluetoothGattCallback)}). This is
	 * to make it quick as the user most probably waits for a quick response.
	 * However, if this method returned true during first connection and the link was lost,
	 * the manager will try to reconnect to it using {@link BluetoothGatt#connect()} which forces
	 * autoConnect to true.
	 *
	 * @return The AutoConnect flag value.
	 * @deprecated Use {@link ConnectRequest#useAutoConnect(boolean)} instead.
	 */
	@Deprecated
	protected boolean shouldAutoConnect() {
		return false;
	}

	/**
	 * Returns whether the device cache should be cleared after the device disconnected,
	 * before calling {@link BluetoothGatt#close()}. By default it returns false.
	 * <p>
	 * If the returned value is true, the next time the Android device will connect to
	 * this peripheral the services will be discovered again. If false, the services
	 * will be obtained from the cache.
	 * <p>
	 * Note, that the {@link BluetoothGatt#refresh()} method is not in the public API and it
	 * is not recommended to use this. However, as Android is caching services of all devices,
	 * even if they are not bonded and have Service Changed characteristic, it may necessary to
	 * clear the cache manually.
	 * <p>
	 * On older Android versions clearing device cache helped with connection stability.
	 * It was common to get error 133 on the second and following connections when services were
	 * obtained from the cache. However, full service discovery takes time and consumes peripheral's
	 * battery.
	 *
	 * @return True, if the device cache should be cleared after the device disconnects or false,
	 * (default) if the cached value be used.
	 */
	@SuppressWarnings("JavadocReference")
	protected boolean shouldClearCacheWhenDisconnected() {
		return false;
	}

	/**
	 * The onConnectionStateChange event is triggered just after the Android connects to a device.
	 * In case of bonded devices, the encryption is reestablished AFTER this callback is called.
	 * Moreover, when the device has Service Changed indication enabled, and the list of services
	 * has changed (e.g. using the DFU), the indication is received few hundred milliseconds later,
	 * depending on the connection interval.
	 * When received, Android will start performing a service discovery operation, internally,
	 * and will NOT notify the app that services has changed.
	 * <p>
	 * If the gatt.discoverServices() method would be invoked here with no delay, if would return
	 * cached services, as the SC indication wouldn't be received yet. Therefore, we have to
	 * postpone the service discovery operation until we are (almost, as there is no such callback)
	 * sure, that it has been handled. It should be greater than the time from
	 * LLCP Feature Exchange to ATT Write for Service Change indication.
	 * <p>
	 * If your device does not use Service Change indication (for example does not have DFU)
	 * the delay may be 0.
	 * <p>
	 * Please calculate the proper delay that will work in your solution.
	 * <p>
	 * For devices that are not bonded, but support paiing, a small delay is required on some
	 * older Android versions (Nexus 4 with Android 5.1.1) when the device will send pairing
	 * request just after connection. If so, we want to wait with the service discovery until
	 * bonding is complete.
	 * <p>
	 * The default implementation returns 1600 ms for bonded and 300 ms when the device is not
	 * bonded to be compatible with older versions of the library.
	 */
	@IntRange(from = 0)
	protected int getServiceDiscoveryDelay(final boolean bonded) {
		return bonded ? 1600 : 300;
	}

	/**
	 * Creates a Connect request that will try to connect to the given Bluetooth LE device.
	 * Call {@link ConnectRequest#enqueue()} or {@link ConnectRequest#await()} in order to execute
	 * the request.
	 * <p>
	 * This method returns a {@link ConnectRequest} which can be used to set completion
	 * and failure callbacks. The completion callback (done) will be called after the initialization
	 * is complete, after {@link BleManagerCallbacks#onDeviceReady(BluetoothDevice)} has been
	 * called.
	 * <p>
	 * Calling {@link ConnectRequest#await()} will make this request
	 * synchronous (the callbacks set will be ignored, instead the synchronous method will
	 * return or throw an exception).
	 * <p>
	 * For asynchronous call usage, {@link ConnectRequest#enqueue()} must be called on the returned
	 * request.
	 * <p>
	 * The callbacks observer must be set with {@link #setGattCallbacks(BleManagerCallbacks)}
	 * before calling this method.
	 *
	 * @param device a device to connect to.
	 * @return The connect request.
	 */
	@SuppressWarnings("ConstantConditions")
	@NonNull
	public final ConnectRequest connect(@NonNull final BluetoothDevice device) {
		if (mCallbacks == null) {
			throw new NullPointerException("Set callbacks using setGattCallbacks(E callbacks) before connecting");
		}
		if (device == null) {
			throw new NullPointerException("Bluetooth device not specified");
		}
		return Request.connect(device)
				.useAutoConnect(shouldAutoConnect())
				.setManager(this);
	}

	/**
	 * Creates a Connect request that will try to connect to the given Bluetooth LE device using
	 * preferred PHY. Call {@link ConnectRequest#enqueue()} or {@link ConnectRequest#await()}
	 * in order to execute the request.
	 * <p>
	 * This method returns a {@link ConnectRequest} which can be used to set completion
	 * and failure callbacks. The completion callback will be called after the initialization
	 * is complete, after {@link BleManagerCallbacks#onDeviceReady(BluetoothDevice)} has been
	 * called.
	 * <p>
	 * Calling {@link ConnectRequest#await()} will make this request
	 * synchronous (the callbacks set will be ignored, instead the synchronous method will
	 * return or throw an exception).
	 * <p>
	 * For asynchronous call usage, {@link ConnectRequest#enqueue()} must be called on the returned
	 * request.
	 * <p>
	 * The callbacks observer must be set with {@link #setGattCallbacks(BleManagerCallbacks)}
	 * before calling this method.
	 *
	 * @param device a device to connect to.
	 * @param phy    preferred PHY for connections to remote LE device. Bitwise OR of any of
	 *               {@link PhyRequest#PHY_LE_1M_MASK}, {@link PhyRequest#PHY_LE_2M_MASK},
	 *               and {@link PhyRequest#PHY_LE_CODED_MASK}. This option does not take effect
	 *               if {@code autoConnect} is set to true. PHY 2M and Coded are supported
	 *               on newer devices running Android Oreo or newer.
	 * @return The connect request.
	 * @deprecated Use {@link #connect(BluetoothDevice)} instead and set preferred PHY using
	 * {@link ConnectRequest#usePreferredPhy(int)}.
	 */
	@SuppressWarnings("ConstantConditions")
	@NonNull
	@Deprecated
	public final ConnectRequest connect(@NonNull final BluetoothDevice device, @PhyMask final int phy) {
		if (mCallbacks == null) {
			throw new NullPointerException("Set callbacks using setGattCallbacks(E callbacks) before connecting");
		}
		if (device == null) {
			throw new NullPointerException("Bluetooth device not specified");
		}
		return Request.connect(device)
				.usePreferredPhy(phy)
				.useAutoConnect(shouldAutoConnect())
				.setManager(this);
	}

	@MainThread
	private boolean internalConnect(@NonNull final BluetoothDevice device,
									@Nullable final ConnectRequest connectRequest) {
		final boolean bluetoothEnabled = BluetoothAdapter.getDefaultAdapter().isEnabled();
		if (mConnected || !bluetoothEnabled) {
			final BluetoothDevice currentDevice = mBluetoothDevice;
			if (bluetoothEnabled && currentDevice != null && currentDevice.equals(device)) {
				mConnectRequest.notifySuccess(device);
			} else {
				// We can't return false here, as the request would be notified with
				// mBluetoothDevice instance instead, and that may be null or a wrong device.
				if (mConnectRequest != null) {
					mConnectRequest.notifyFail(device,
							bluetoothEnabled ?
									FailCallback.REASON_REQUEST_FAILED :
									FailCallback.REASON_BLUETOOTH_DISABLED);
				} // else, the request was already failed by the Bluetooth state receiver
			}
			mConnectRequest = null;
			if (mGattCallback != null) {
				mGattCallback.nextRequest(true);
			}
			return true;
		}

		synchronized (mLock) {
			if (mBluetoothGatt != null) {
				// There are 2 ways of reconnecting to the same device:
				// 1. Reusing the same BluetoothGatt object and calling connect() - this will force
				//    the autoConnect flag to true
				// 2. Closing it and reopening a new instance of BluetoothGatt object.
				// The gatt.close() is an asynchronous method. It requires some time before it's
				// finished and device.connectGatt(...) can't be called immediately or service
				// discovery may never finish on some older devices (Nexus 4, Android 5.0.1).
				// If shouldAutoConnect() method returned false we can't call gatt.connect() and
				// have to close gatt and open it again.
				if (!mInitialConnection) {
					log(Log.DEBUG, "gatt.close()");
					mBluetoothGatt.close();
					mBluetoothGatt = null;
					try {
						log(Log.DEBUG, "wait(200)");
						Thread.sleep(200); // Is 200 ms enough?
					} catch (final InterruptedException e) {
						// Ignore
					}
				} else {
					// Instead, the gatt.connect() method will be used to reconnect to the same device.
					// This method forces autoConnect = true even if the gatt was created with this
					// flag set to false.
					mInitialConnection = false;
					mConnectionTime = 0L; // no timeout possible when autoConnect used
					mConnectionState = BluetoothGatt.STATE_CONNECTING;
					log(Log.VERBOSE, "Connecting...");
					mCallbacks.onDeviceConnecting(device);
					log(Log.DEBUG, "gatt.connect()");
					mBluetoothGatt.connect();
					return true;
				}
			} else {
				// Register bonding broadcast receiver
				mContext.registerReceiver(mBluetoothStateBroadcastReceiver,
						new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
				mContext.registerReceiver(mBondingBroadcastReceiver,
						new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
				mContext.registerReceiver(mPairingRequestBroadcastReceiver,
						//BluetoothDevice.ACTION_PAIRING_REQUEST
						new IntentFilter("android.bluetooth.device.action.PAIRING_REQUEST"));
			}
		}

		// This should not happen in normal circumstances, but may, when Bluetooth was turned off
		// when retrying to create a connection.
		if (connectRequest == null)
			return false;
		final boolean shouldAutoConnect = connectRequest.shouldAutoConnect();
		// We will receive Link Loss events only when the device is connected with autoConnect=true.
		mUserDisconnected = !shouldAutoConnect;
		// The first connection will always be done with autoConnect = false to make the connection quick.
		// If the shouldAutoConnect() method returned true, the manager will automatically try to
		// reconnect to this device on link loss.
		if (shouldAutoConnect) {
			mInitialConnection = true;
		}
		mBluetoothDevice = device;
		mGattCallback.setHandler(mHandler);
		log(Log.VERBOSE, connectRequest.isFirstAttempt() ? "Connecting..." : "Retrying...");
		mConnectionState = BluetoothGatt.STATE_CONNECTING;
		mCallbacks.onDeviceConnecting(device);
		mConnectionTime = SystemClock.elapsedRealtime();
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
			// connectRequest will never be null here.
			final int preferredPhy = connectRequest.getPreferredPhy();
			log(Log.DEBUG, "gatt = device.connectGatt(autoConnect = false, TRANSPORT_LE, "
					+ phyMaskToString(preferredPhy) + ")");
			// A variant of connectGatt with Handled can't be used here.
			// Check https://github.com/NordicSemiconductor/Android-BLE-Library/issues/54
			mBluetoothGatt = device.connectGatt(mContext, false, mGattCallback,
					BluetoothDevice.TRANSPORT_LE, preferredPhy/*, mHandler*/);
		} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
			log(Log.DEBUG, "gatt = device.connectGatt(autoConnect = false, TRANSPORT_LE)");
			mBluetoothGatt = device.connectGatt(mContext, false, mGattCallback,
					BluetoothDevice.TRANSPORT_LE);
		} else {
			log(Log.DEBUG, "gatt = device.connectGatt(autoConnect = false)");
			mBluetoothGatt = device.connectGatt(mContext, false, mGattCallback);
		}
		return true;
	}

	/**
	 * Disconnects from the device or cancels the pending connection attempt.
	 * Does nothing if device was not connected.
	 *
	 * @return The disconnect request. The completion callback will be called after the device
	 * has disconnected and the connection was closed. If the device was not connected,
	 * the completion callback will be called immediately with device parameter set to null.
	 */
	@NonNull
	public final DisconnectRequest disconnect() {
		return Request.disconnect().setManager(this);
	}

	@MainThread
	private boolean internalDisconnect() {
		mUserDisconnected = true;
		mInitialConnection = false;
		mReady = false;

		if (mBluetoothGatt != null) {
			mConnectionState = BluetoothGatt.STATE_DISCONNECTING;
			log(Log.VERBOSE, mConnected ? "Disconnecting..." : "Cancelling connection...");
			mCallbacks.onDeviceDisconnecting(mBluetoothGatt.getDevice());
			final boolean wasConnected = mConnected;
			log(Log.DEBUG, "gatt.disconnect()");
			mBluetoothGatt.disconnect();

			if (wasConnected)
				return true;

			// If the device wasn't connected, there will be no callback after calling
			// gatt.disconnect(), the connection attempt will be stopped.
			mConnectionState = BluetoothGatt.STATE_DISCONNECTED;
			log(Log.INFO, "Disconnected");
			mCallbacks.onDeviceDisconnected(mBluetoothGatt.getDevice());
		}
		// mRequest may be of type DISCONNECT or CONNECT (timeout).
		// For the latter, it has already been notified with REASON_TIMEOUT.
		if (mRequest != null && mRequest.type == Request.Type.DISCONNECT) {
			if (mBluetoothDevice != null)
				mRequest.notifySuccess(mBluetoothDevice);
			else
				mRequest.notifyInvalidRequest();
		}
		if (mGattCallback != null) {
			mGattCallback.nextRequest(true);
		}
		return true;
	}

	/**
	 * Closes and releases resources. This method will be called automatically after
	 * calling {@link #disconnect()}. When the device disconnected with link loss and
	 * {@link #shouldAutoConnect()} returned true you have call this method to close the connection.
	 */
	public void close() {
		try {
			mContext.unregisterReceiver(mBluetoothStateBroadcastReceiver);
			mContext.unregisterReceiver(mBondingBroadcastReceiver);
			mContext.unregisterReceiver(mPairingRequestBroadcastReceiver);
		} catch (final Exception e) {
			// the receiver must have been not registered or unregistered before.
		}
		synchronized (mLock) {
			if (mBluetoothGatt != null) {
				if (shouldClearCacheWhenDisconnected()) {
					if (internalRefreshDeviceCache()) {
						log(Log.INFO, "Cache refreshed");
					} else {
						log(Log.WARN, "Refreshing failed");
					}
				}
				log(Log.DEBUG, "gatt.close()");
				mBluetoothGatt.close();
				mBluetoothGatt = null;
			}
			mConnected = false;
			mInitialConnection = false;
			mReliableWriteInProgress = false;
			mNotificationCallbacks.clear();
			mConnectionState = BluetoothGatt.STATE_DISCONNECTED;
			if (mGattCallback != null) {
				// close() is called in notifyDeviceDisconnected, which may enqueue new requests.
				// Setting this flag to false would allow to enqueue a new request before the
				// current one ends processing. The following line should not be uncommented.
				// mGattCallback.mOperationInProgress = false;
				mGattCallback.cancelQueue();
				mGattCallback.mInitQueue = null;
			}
			mGattCallback = null;
			mBluetoothDevice = null;
		}
	}

	/**
	 * Returns a request to create bond with the device. The device must be first set using
	 * {@link #connect(BluetoothDevice)} which will try to connect to the device.
	 * If you need to pair with a device before connecting to it you may do it without
	 * the use of BleManager object and connect after bond is established.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @return The request.
	 */
	@NonNull
	protected Request createBond() {
		return Request.createBond().setManager(this);
	}

	@MainThread
	private boolean internalCreateBond() {
		final BluetoothDevice device = mBluetoothDevice;
		if (device == null)
			return false;

		log(Log.VERBOSE, "Starting pairing...");

		if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
			log(Log.WARN, "Device already bonded");
			mRequest.notifySuccess(device);
			mGattCallback.nextRequest(true);
			return true;
		}

		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
			log(Log.DEBUG, "device.createBond()");
			return device.createBond();
		} else {
			/*
			 * There is a createBond() method in BluetoothDevice class but for now it's hidden.
			 * We will call it using reflections. It has been revealed in KitKat (Api19).
			 */
			try {
				final Method createBond = device.getClass().getMethod("createBond");
				log(Log.DEBUG, "device.createBond() (hidden)");
				return (Boolean) createBond.invoke(device);
			} catch (final Exception e) {
				Log.w(TAG, "An exception occurred while creating bond", e);
			}
		}
		return false;
	}

	/**
	 * Enqueues removing bond information. When the device was bonded and the bond
	 * information was successfully removed, the device will disconnect.
	 * Note, that this will not remove the bond information from the connected device!
	 * <p>
	 * The success callback will be called after the device get disconnected,
	 * when the {@link BluetoothDevice#getBondState()} changes to {@link BluetoothDevice#BOND_NONE}.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @return The request.
	 */
	@NonNull
	protected Request removeBond() {
		return Request.removeBond().setManager(this);
	}

	@MainThread
	private boolean internalRemoveBond() {
		final BluetoothDevice device = mBluetoothDevice;
		if (device == null)
			return false;

		log(Log.VERBOSE, "Removing bond information...");

		if (device.getBondState() == BluetoothDevice.BOND_NONE) {
			log(Log.WARN, "Device is not bonded");
			mRequest.notifySuccess(device);
			mGattCallback.nextRequest(true);
			return true;
		}

		/*
		 * There is a removeBond() method in BluetoothDevice class but for now it's hidden.
		 * We will call it using reflections.
		 */
		try {
			//noinspection JavaReflectionMemberAccess
			final Method removeBond = device.getClass().getMethod("removeBond");
			log(Log.DEBUG, "device.removeBond() (hidden)");
			return (Boolean) removeBond.invoke(device);
		} catch (final Exception e) {
			Log.w(TAG, "An exception occurred while removing bond", e);
		}
		return false;
	}

	/**
	 * When the device is bonded and has the Generic Attribute service and the Service Changed
	 * characteristic this method enables indications on this characteristic.
	 * In case one of the requirements is not fulfilled this method returns <code>false</code>.
	 *
	 * @return <code>true</code> when the request has been sent, <code>false</code> when the device
	 * is not bonded, does not have the Generic Attribute service, the GA service does not have
	 * the Service Changed characteristic or this characteristic does not have the CCCD.
	 */
	private boolean ensureServiceChangedEnabled() {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || !mConnected)
			return false;

		// The Service Changed indications have sense only on bonded devices.
		final BluetoothDevice device = gatt.getDevice();
		if (device.getBondState() != BluetoothDevice.BOND_BONDED)
			return false;

		final BluetoothGattService gaService = gatt.getService(GENERIC_ATTRIBUTE_SERVICE);
		if (gaService == null)
			return false;

		final BluetoothGattCharacteristic scCharacteristic =
				gaService.getCharacteristic(SERVICE_CHANGED_CHARACTERISTIC);
		if (scCharacteristic == null)
			return false;

		log(Log.INFO, "Service Changed characteristic found on a bonded device");
		return internalEnableIndications(scCharacteristic);
	}

	/**
	 * Returns the callback that is registered for value changes (notifications) of given
	 * characteristic. After assigning the notifications callback, notifications must be
	 * enabled using {@link #enableNotifications(BluetoothGattCharacteristic)}.
	 * This applies also when they were already enabled on the remote side.
	 * <p>
	 * To remove the callback, disable notifications using
	 * {@link #disableNotifications(BluetoothGattCharacteristic)}.
	 *
	 * @param characteristic characteristic to bind the callback with. If null, the returned
	 *                       callback will not be null, but will not be used.
	 * @return The callback.
	 */
	@MainThread
	@NonNull
	protected ValueChangedCallback setNotificationCallback(@Nullable final BluetoothGattCharacteristic characteristic) {
		ValueChangedCallback callback = mNotificationCallbacks.get(characteristic);
		if (callback == null) {
			callback = new ValueChangedCallback();
			if (characteristic != null) {
				mNotificationCallbacks.put(characteristic, callback);
			}
		}
		return callback.free();
	}

	/**
	 * Returns the callback that is registered for value changes (indications) of given
	 * characteristic. After assigning the notifications callback, indications must be
	 * enabled using {@link #enableIndications(BluetoothGattCharacteristic)}.
	 * This applies also when they were already enabled on the remote side.
	 * <p>
	 * To remove the callback, disable indications using
	 * {@link #disableIndications(BluetoothGattCharacteristic)}.
	 *
	 * @param characteristic characteristic to bind the callback with. If null, the returned
	 *                       callback will not be null, but will not be used.
	 * @return The callback.
	 */
	@NonNull
	protected ValueChangedCallback setIndicationCallback(@Nullable final BluetoothGattCharacteristic characteristic) {
		return setNotificationCallback(characteristic);
	}

	/**
	 * Sets a one-time callback that will be notified when the value of the given characteristic
	 * changes. This is a blocking request, so the next request will be executed after the
	 * notification was received.
	 * <p>
	 * If {@link WaitForValueChangedRequest#merge(DataMerger)} was used, the whole message will be
	 * completed before the callback is notified.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param characteristic the characteristic that value is expect to change.
	 * @return The callback.
	 */
	@NonNull
	protected WaitForValueChangedRequest waitForNotification(@NonNull final BluetoothGattCharacteristic characteristic) {
		return Request.newWaitForNotificationRequest(characteristic).setManager(this);
	}

	/**
	 * Sets a one-time callback that will be notified when the value of the given characteristic
	 * changes. This is a blocking request, so the next request will be executed after the
	 * indication was received.
	 * <p>
	 * If {@link WaitForValueChangedRequest#merge(DataMerger)} was used, the whole message will be
	 * completed before the callback is notified.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param characteristic the characteristic that value is expect to change.
	 * @return The callback.
	 */
	@NonNull
	protected WaitForValueChangedRequest waitForIndication(@NonNull final BluetoothGattCharacteristic characteristic) {
		return Request.newWaitForIndicationRequest(characteristic).setManager(this);
	}

	/**
	 * Enables notifications on given characteristic.
	 * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)}
	 * callback will be called.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param characteristic the characteristic to be enabled.
	 * @return The request.
	 */
	@NonNull
	protected WriteRequest enableNotifications(
			@Nullable final BluetoothGattCharacteristic characteristic) {
		return Request.newEnableNotificationsRequest(characteristic).setManager(this);
	}

	@MainThread
	private boolean internalEnableNotifications(final BluetoothGattCharacteristic characteristic) {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || characteristic == null || !mConnected)
			return false;

		final BluetoothGattDescriptor descriptor = getCccd(characteristic, BluetoothGattCharacteristic.PROPERTY_NOTIFY);
		if (descriptor != null) {
			log(Log.DEBUG, "gatt.setCharacteristicNotification(" + characteristic.getUuid() + ", true)");
			gatt.setCharacteristicNotification(characteristic, true);

			descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
			log(Log.VERBOSE, "Enabling notifications for " + characteristic.getUuid());
			log(Log.DEBUG, "gatt.writeDescriptor(" + CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID + ", value=0x01-00)");
			return internalWriteDescriptorWorkaround(descriptor);
		}
		return false;
	}

	/**
	 * Disables notifications on given characteristic.
	 * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)}
	 * callback will be called.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param characteristic the characteristic to be disabled.
	 * @return The request.
	 */
	@NonNull
	protected WriteRequest disableNotifications(@Nullable final BluetoothGattCharacteristic characteristic) {
		return Request.newDisableNotificationsRequest(characteristic).setManager(this);
	}

	@MainThread
	private boolean internalDisableNotifications(final BluetoothGattCharacteristic characteristic) {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || characteristic == null || !mConnected)
			return false;

		final BluetoothGattDescriptor descriptor = getCccd(characteristic, BluetoothGattCharacteristic.PROPERTY_NOTIFY);
		if (descriptor != null) {
			log(Log.DEBUG, "gatt.setCharacteristicNotification(" + characteristic.getUuid() + ", false)");
			gatt.setCharacteristicNotification(characteristic, false);

			descriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
			log(Log.VERBOSE, "Disabling notifications and indications for " + characteristic.getUuid());
			log(Log.DEBUG, "gatt.writeDescriptor(" + CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID + ", value=0x00-00)");
			return internalWriteDescriptorWorkaround(descriptor);
		}
		return false;
	}

	/**
	 * Enables indications on given characteristic.
	 * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)}
	 * callback will be called.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param characteristic the characteristic to be enabled.
	 * @return The request.
	 */
	@NonNull
	protected WriteRequest enableIndications(@Nullable final BluetoothGattCharacteristic characteristic) {
		return Request.newEnableIndicationsRequest(characteristic).setManager(this);
	}

	@MainThread
	private boolean internalEnableIndications(final BluetoothGattCharacteristic characteristic) {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || characteristic == null || !mConnected)
			return false;

		final BluetoothGattDescriptor descriptor = getCccd(characteristic, BluetoothGattCharacteristic.PROPERTY_INDICATE);
		if (descriptor != null) {
			log(Log.DEBUG, "gatt.setCharacteristicNotification(" + characteristic.getUuid() + ", true)");
			gatt.setCharacteristicNotification(characteristic, true);

			descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
			log(Log.VERBOSE, "Enabling indications for " + characteristic.getUuid());
			log(Log.DEBUG, "gatt.writeDescriptor(" + CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID + ", value=0x02-00)");
			return internalWriteDescriptorWorkaround(descriptor);
		}
		return false;
	}

	/**
	 * Disables indications on given characteristic.
	 * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)}
	 * callback will be called.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param characteristic the characteristic to be disabled.
	 * @return The request.
	 */
	@NonNull
	protected WriteRequest disableIndications(@Nullable final BluetoothGattCharacteristic characteristic) {
		return Request.newDisableIndicationsRequest(characteristic).setManager(this);
	}

	@MainThread
	private boolean internalDisableIndications(final BluetoothGattCharacteristic characteristic) {
		// This writes exactly the same settings so do not duplicate code.
		return internalDisableNotifications(characteristic);
	}

	/**
	 * Returns the Client Characteristic Config Descriptor if the characteristic has the
	 * required property. It may return null if the CCCD is not there.
	 *
	 * @param characteristic   the characteristic to look the CCCD in.
	 * @param requiredProperty the required property: {@link BluetoothGattCharacteristic#PROPERTY_NOTIFY}
	 *                         or {@link BluetoothGattCharacteristic#PROPERTY_INDICATE}.
	 * @return The CCC descriptor or null if characteristic is null, if it doesn't have the
	 * required property, or if the CCCD is missing.
	 */
	private static BluetoothGattDescriptor getCccd(@Nullable final BluetoothGattCharacteristic characteristic,
												   final int requiredProperty) {
		if (characteristic == null)
			return null;

		// Check characteristic property
		final int properties = characteristic.getProperties();
		if ((properties & requiredProperty) == 0)
			return null;

		return characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID);
	}

	/**
	 * Sends the read request to the given characteristic.
	 * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)}
	 * callback will be called.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param characteristic the characteristic to read.
	 * @return The request.
	 */
	@NonNull
	protected ReadRequest readCharacteristic(@Nullable final BluetoothGattCharacteristic characteristic) {
		return Request.newReadRequest(characteristic).setManager(this);
	}

	@MainThread
	private boolean internalReadCharacteristic(final BluetoothGattCharacteristic characteristic) {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || characteristic == null || !mConnected)
			return false;

		// Check characteristic property.
		final int properties = characteristic.getProperties();
		if ((properties & BluetoothGattCharacteristic.PROPERTY_READ) == 0)
			return false;

		log(Log.VERBOSE, "Reading characteristic " + characteristic.getUuid());
		log(Log.DEBUG, "gatt.readCharacteristic(" + characteristic.getUuid() + ")");
		return gatt.readCharacteristic(characteristic);
	}

	/**
	 * Writes the given data to the characteristic. The write type is taken from the characteristic.
	 * <p>
	 * Use {@link WriteRequest#split() split()} or
	 * {@link WriteRequest#split(DataSplitter) split(DataSplitter)} on the returned
	 * {@link WriteRequest} if data should be automatically split into multiple packets.
	 * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)}
	 * callback will be called.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param characteristic the characteristic to write to.
	 * @param data           data to be written to the characteristic.
	 * @return The request.
	 */
	@NonNull
	protected WriteRequest writeCharacteristic(@Nullable final BluetoothGattCharacteristic characteristic,
											   @Nullable final Data data) {
		return Request.newWriteRequest(characteristic, data != null ? data.getValue() : null)
				.setManager(this);
	}

	/**
	 * Writes the given data to the characteristic. The write type is taken from the characteristic.
	 * <p>
	 * Use {@link WriteRequest#split() split()} or
	 * {@link WriteRequest#split(DataSplitter) split(ValueSplitter)} on the returned
	 * {@link WriteRequest} if data should be automatically split into multiple packets.
	 * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)}
	 * callback will be called.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param characteristic the characteristic to write to.
	 * @param data           data to be written to the characteristic.
	 * @return The request.
	 */
	@NonNull
	protected WriteRequest writeCharacteristic(@Nullable final BluetoothGattCharacteristic characteristic,
											   @Nullable final byte[] data) {
		return Request.newWriteRequest(characteristic, data).setManager(this);
	}

	/**
	 * Writes at most length bytes from offset at given data to the characteristic.
	 * The write type is taken from the characteristic.
	 * <p>
	 * Use {@link WriteRequest#split() split()} or
	 * {@link WriteRequest#split(DataSplitter) split(ValueSplitter)} on the returned
	 * {@link WriteRequest} if data should be automatically split into multiple packets.
	 * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)}
	 * callback will be called.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param characteristic the characteristic to write to.
	 * @param data           data to be written to the characteristic.
	 * @param offset         index of the first byte to be sent.
	 * @param length         number of bytes to be sent.
	 * @return The request.
	 */
	@NonNull
	protected WriteRequest writeCharacteristic(@Nullable final BluetoothGattCharacteristic characteristic,
											   @Nullable final byte[] data, final int offset, final int length) {
		return Request.newWriteRequest(characteristic, data, offset, length).setManager(this);
	}

	@MainThread
	private boolean internalWriteCharacteristic(final BluetoothGattCharacteristic characteristic) {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || characteristic == null || !mConnected)
			return false;

		// Check characteristic property.
		final int properties = characteristic.getProperties();
		if ((properties & (BluetoothGattCharacteristic.PROPERTY_WRITE |
				BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)) == 0)
			return false;

		log(Log.VERBOSE, "Writing characteristic " + characteristic.getUuid() +
				" (" + writeTypeToString(characteristic.getWriteType()) + ")");
		log(Log.DEBUG, "gatt.writeCharacteristic(" + characteristic.getUuid() + ")");
		return gatt.writeCharacteristic(characteristic);
	}

	/**
	 * Sends the read request to the given descriptor.
	 * If the descriptor is null, the {@link Request#fail(FailCallback) fail(FailCallback)}
	 * callback will be called.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param descriptor the descriptor to read.
	 * @return The request.
	 */
	@NonNull
	protected ReadRequest readDescriptor(@Nullable final BluetoothGattDescriptor descriptor) {
		return Request.newReadRequest(descriptor).setManager(this);
	}

	@MainThread
	private boolean internalReadDescriptor(final BluetoothGattDescriptor descriptor) {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || descriptor == null || !mConnected)
			return false;

		log(Log.VERBOSE, "Reading descriptor " + descriptor.getUuid());
		log(Log.DEBUG, "gatt.readDescriptor(" + descriptor.getUuid() + ")");
		return gatt.readDescriptor(descriptor);
	}

	/**
	 * Writes the given data to the descriptor.
	 * <p>
	 * Use {@link WriteRequest#split() split()} or
	 * {@link WriteRequest#split(DataSplitter) split(ValueSplitter)} on the returned
	 * {@link WriteRequest} if data should be automatically split into multiple packets.
	 * If the descriptor is null, the {@link Request#fail(FailCallback) fail(FailCallback)}
	 * callback will be called.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param descriptor the descriptor to write to.
	 * @param data       data to be written to the descriptor.
	 * @return The request.
	 */
	@NonNull
	protected WriteRequest writeDescriptor(@Nullable final BluetoothGattDescriptor descriptor,
										   @Nullable final Data data) {
		return Request.newWriteRequest(descriptor, data != null ? data.getValue() : null)
				.setManager(this);
	}

	/**
	 * Writes the given data to the descriptor.
	 * <p>
	 * Use {@link WriteRequest#split() split()} or
	 * {@link WriteRequest#split(DataSplitter) split(ValueSplitter)} on the returned
	 * {@link WriteRequest} if data should be automatically split into multiple packets.
	 * If the descriptor is null, the {@link Request#fail(FailCallback) fail(FailCallback)}
	 * callback will be called.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param descriptor the descriptor to write to.
	 * @param data       data to be written to the descriptor.
	 * @return The request.
	 */
	@NonNull
	protected WriteRequest writeDescriptor(@Nullable final BluetoothGattDescriptor descriptor,
										   @Nullable final byte[] data) {
		return Request.newWriteRequest(descriptor, data).setManager(this);
	}

	/**
	 * Writes at most length bytes from offset at given data to the descriptor.
	 * <p>
	 * Use {@link WriteRequest#split() split()} or
	 * {@link WriteRequest#split(DataSplitter) split(ValueSplitter)} on the returned
	 * {@link WriteRequest} if data should be automatically split into multiple packets.
	 * If the descriptor is null, the {@link Request#fail(FailCallback) fail(FailCallback)}
	 * callback will be called.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param descriptor the descriptor to write to.
	 * @param data       data to be written to the descriptor.
	 * @param offset     index of the first byte to be sent.
	 * @param length     number of bytes to be sent.
	 * @return The request.
	 */
	@NonNull
	protected WriteRequest writeDescriptor(@Nullable final BluetoothGattDescriptor descriptor,
										   @Nullable final byte[] data, final int offset,
										   final int length) {
		return Request.newWriteRequest(descriptor, data, offset, length).setManager(this);
	}

	@MainThread
	private boolean internalWriteDescriptor(final BluetoothGattDescriptor descriptor) {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || descriptor == null || !mConnected)
			return false;

		log(Log.VERBOSE, "Writing descriptor " + descriptor.getUuid());
		log(Log.DEBUG, "gatt.writeDescriptor(" + descriptor.getUuid() + ")");
		return internalWriteDescriptorWorkaround(descriptor);
	}

	/**
	 * Creates an atomic request queue. The requests from the queue will be executed in order.
	 * This is useful when more then one thread may add requests and you want some of them to
	 * be executed together.
	 *
	 * @return The request.
	 */
	@NonNull
	protected RequestQueue beginAtomicRequestQueue() {
		return new RequestQueue().setManager(this);
	}

	/**
	 * Begins the Reliable Write sub-procedure. Requests that need to be performed reliably
	 * should be enqueued with {@link ReliableWriteRequest#add(Operation)} instead of using
	 * {@link Request#enqueue()}. The library will verify all Write operations and will
	 * automatically abort the Reliable Write procedure when the returned data mismatch with the
	 * data sent. When all requests enqueued in the {@link ReliableWriteRequest} were completed,
	 * the Reliable Write will be automatically executed.
	 * <p>
	 * Long Write will not work when Reliable Write is in progress. The library will make sure
	 * that {@link WriteRequest#split()} was called for all {@link WriteRequest} packets, had
	 * they not been assigned other splitter.
	 * <p>
	 * At least one Write operation must be executed before executing or aborting, otherwise the
	 * {@link GattError#GATT_INVALID_OFFSET} error will be reported. Because of that, enqueueing
	 * a {@link ReliableWriteRequest} without any operations does nothing.
	 * <p>
	 * Example of usage:
	 * <pre>
	 *     beginReliableWrite()
	 *           .add(writeCharacteristic(someCharacteristic, someData)
	 *                   .fail(...)
	 *                   .done(...))
	 *           // Non-write requests are also possible
	 *           .add(requestMtu(200))
	 *           // Data will be written in the same order
	 *           .add(writeCharacteristic(someCharacteristic, differentData))
	 *           // This will return the OLD data, not 'differentData', as the RW wasn't executed!
	 *           .add(readCharacteristic(someCharacteristic).with(callback))
	 *           // Multiple characteristics may be written during a single RW
	 *           .add(writeCharacteristic(someOtherCharacteristic, importantData))
	 *        // Finally, enqueue the Reliable Write request in BleManager
	 *     	  .enqueue();
	 * </pre>
	 *
	 * @return The request.
	 */
	@NonNull
	protected ReliableWriteRequest beginReliableWrite() {
		return Request.newReliableWriteRequest().setManager(this);
	}

	@MainThread
	private boolean internalBeginReliableWrite() {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || !mConnected)
			return false;

		// Reliable Write can't be before the old one isn't executed or aborted.
		if (mReliableWriteInProgress)
			return true;

		log(Log.VERBOSE, "Beginning reliable write...");
		log(Log.DEBUG, "gatt.beginReliableWrite()");
		return mReliableWriteInProgress = gatt.beginReliableWrite();
	}

	@MainThread
	private boolean internalExecuteReliableWrite() {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || !mConnected)
			return false;

		if (!mReliableWriteInProgress)
			return false;

		log(Log.VERBOSE, "Executing reliable write...");
		log(Log.DEBUG, "gatt.executeReliableWrite()");
		return gatt.executeReliableWrite();
	}

	@MainThread
	private boolean internalAbortReliableWrite() {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || !mConnected)
			return false;

		if (!mReliableWriteInProgress)
			return false;

		log(Log.VERBOSE, "Aborting reliable write...");
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
			log(Log.DEBUG, "gatt.abortReliableWrite()");
			gatt.abortReliableWrite();
		} else {
			log(Log.DEBUG, "gatt.abortReliableWrite(device)");
			gatt.abortReliableWrite(mBluetoothDevice);
		}
		return true;
	}

	/**
	 * Returns true if {@link BluetoothGatt#beginReliableWrite()} has been called and
	 * the Reliable Write hasn't been executed nor aborted yet.
	 */
	protected final boolean isReliableWriteInProgress() {
		return mReliableWriteInProgress;
	}

	/**
	 * Reads the battery level from the device.
	 *
	 * @deprecated Use {@link #readCharacteristic(BluetoothGattCharacteristic)} instead.
	 */
	@SuppressWarnings("ConstantConditions")
	@Deprecated
	protected void readBatteryLevel() {
		Request.newReadBatteryLevelRequest().setManager(this)
				.with((device, data) -> {
					if (data.size() == 1) {
						final int batteryLevel = data.getIntValue(Data.FORMAT_UINT8, 0);
						log(Log.INFO, "Battery Level received: " + batteryLevel + "%");
						mBatteryValue = batteryLevel;
						final BleManagerGattCallback callback = mGattCallback;
						if (callback != null) {
							callback.onBatteryValueReceived(mBluetoothGatt, batteryLevel);
						}
						mCallbacks.onBatteryValueReceived(device, batteryLevel);
					}
				})
				.enqueue();
	}

	@MainThread
	@Deprecated
	private boolean internalReadBatteryLevel() {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || !mConnected)
			return false;

		final BluetoothGattService batteryService = gatt.getService(BATTERY_SERVICE);
		if (batteryService == null)
			return false;

		final BluetoothGattCharacteristic batteryLevelCharacteristic =
				batteryService.getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC);
		return internalReadCharacteristic(batteryLevelCharacteristic);
	}

	/**
	 * This method enables notifications on the Battery Level characteristic.
	 *
	 * @deprecated Use {@link #setNotificationCallback(BluetoothGattCharacteristic)} and
	 * {@link #enableNotifications(BluetoothGattCharacteristic)} instead.
	 */
	@SuppressWarnings("ConstantConditions")
	@Deprecated
	protected void enableBatteryLevelNotifications() {
		if (mBatteryLevelNotificationCallback == null) {
			mBatteryLevelNotificationCallback = new ValueChangedCallback()
					.with((device, data) -> {
						if (data.size() == 1) {
							final int batteryLevel = data.getIntValue(Data.FORMAT_UINT8, 0);
							mBatteryValue = batteryLevel;
							final BleManagerGattCallback callback = mGattCallback;
							if (callback != null) {
								callback.onBatteryValueReceived(mBluetoothGatt, batteryLevel);
							}
							mCallbacks.onBatteryValueReceived(device, batteryLevel);
						}
					});
		}
		Request.newEnableBatteryLevelNotificationsRequest().setManager(this)
				.done(device -> log(Log.INFO, "Battery Level notifications enabled"))
				.enqueue();
	}

	/**
	 * This method disables notifications on the Battery Level characteristic.
	 *
	 * @deprecated Use {@link #disableNotifications(BluetoothGattCharacteristic)} instead.
	 */
	@Deprecated
	protected void disableBatteryLevelNotifications() {
		Request.newDisableBatteryLevelNotificationsRequest().setManager(this)
				.done(device -> log(Log.INFO, "Battery Level notifications disabled"))
				.enqueue();
	}

	@MainThread
	@Deprecated
	private boolean internalSetBatteryNotifications(final boolean enable) {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || !mConnected)
			return false;

		final BluetoothGattService batteryService = gatt.getService(BATTERY_SERVICE);
		if (batteryService == null)
			return false;

		final BluetoothGattCharacteristic batteryLevelCharacteristic = batteryService.getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC);
		if (enable)
			return internalEnableNotifications(batteryLevelCharacteristic);
		else
			return internalDisableNotifications(batteryLevelCharacteristic);
	}

	/**
	 * There was a bug in Android up to 6.0 where the descriptor was written using parent
	 * characteristic's write type, instead of always Write With Response, as the spec says.
	 * <p>
	 * See: <a href="https://android.googlesource.com/platform/frameworks/base/+/942aebc95924ab1e7ea1e92aaf4e7fc45f695a6c%5E%21/#F0">
	 * https://android.googlesource.com/platform/frameworks/base/+/942aebc95924ab1e7ea1e92aaf4e7fc45f695a6c%5E%21/#F0</a>
	 *
	 * @param descriptor the descriptor to be written
	 * @return the result of {@link BluetoothGatt#writeDescriptor(BluetoothGattDescriptor)}
	 */
	@MainThread
	private boolean internalWriteDescriptorWorkaround(final BluetoothGattDescriptor descriptor) {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || descriptor == null || !mConnected)
			return false;

		final BluetoothGattCharacteristic parentCharacteristic = descriptor.getCharacteristic();
		final int originalWriteType = parentCharacteristic.getWriteType();
		parentCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
		final boolean result = gatt.writeDescriptor(descriptor);
		parentCharacteristic.setWriteType(originalWriteType);
		return result;
	}

	/**
	 * Requests new MTU. On Android Lollipop or newer it will send the MTU request to the connected
	 * device. On older versions of Android the
	 * {@link MtuCallback#onMtuChanged(BluetoothDevice, int)} set with
	 * {@link MtuRequest#with(MtuCallback)} will be called with current MTU value.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @return The request.
	 */
	protected MtuRequest requestMtu(@IntRange(from = 23, to = 517) final int mtu) {
		return Request.newMtuRequest(mtu).setManager(this);
	}

	/**
	 * Returns the current MTU (Maximum Transfer Unit). MTU specifies the maximum number of bytes
	 * that can be sent in a single write operation. 3 bytes are used for internal purposes,
	 * so the maximum size is MTU-3. The value will changed only if requested with
	 * {@link #requestMtu(int)} and a successful callback is received. If the peripheral requests
	 * MTU change, the {@link BluetoothGattCallback#onMtuChanged(BluetoothGatt, int, int)}
	 * callback is not invoked, therefor the returned MTU value will not be correct.
	 * Use {@link android.bluetooth.BluetoothGattServerCallback#onMtuChanged(BluetoothDevice, int)}
	 * to get the callback with right value requested from the peripheral side.
	 *
	 * @return the current MTU value. Default to 23.
	 */
	@IntRange(from = 23, to = 517)
	protected int getMtu() {
		return mMtu;
	}

	/**
	 * This method overrides the MTU value. Use it only when the peripheral has changed MTU and you
	 * received the
	 * {@link android.bluetooth.BluetoothGattServerCallback#onMtuChanged(BluetoothDevice, int)}
	 * callback. If you want to set MTU as a master, use {@link #requestMtu(int)} instead.
	 *
	 * @param mtu the MTU value set by the peripheral.
	 */
	protected void overrideMtu(@IntRange(from = 23, to = 517) final int mtu) {
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
			mMtu = mtu;
		}
	}

	@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
	@MainThread
	private boolean internalRequestMtu(@IntRange(from = 23, to = 517) final int mtu) {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || !mConnected)
			return false;

		log(Log.VERBOSE, "Requesting new MTU...");
		log(Log.DEBUG, "gatt.requestMtu(" + mtu + ")");
		return gatt.requestMtu(mtu);
	}

	/**
	 * Requests the new connection priority. Acceptable values are:
	 * <ol>
	 * <li>{@link BluetoothGatt#CONNECTION_PRIORITY_HIGH}
	 * - Interval: 11.25 -15 ms, latency: 0, supervision timeout: 20 sec,</li>
	 * <li>{@link BluetoothGatt#CONNECTION_PRIORITY_BALANCED}
	 * - Interval: 30 - 50 ms, latency: 0, supervision timeout: 20 sec,</li>
	 * <li>{@link BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER}
	 * - Interval: 100 - 125 ms, latency: 2, supervision timeout: 20 sec.</li>
	 * </ol>
	 * Works only on Android Lollipop or newer. On older system versions will cause
	 * {@link Request#fail(FailCallback)} callback or throw
	 * {@link no.nordicsemi.android.ble.exception.RequestFailedException} with
	 * {@link FailCallback#REASON_REQUEST_FAILED} status if called synchronously.
	 * Starting from Android Oreo you may get a callback with the interval, latency and timeout
	 * using {@link ConnectionPriorityRequest#with(ConnectionPriorityCallback)}.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param priority one of: {@link BluetoothGatt#CONNECTION_PRIORITY_HIGH},
	 *                 {@link BluetoothGatt#CONNECTION_PRIORITY_BALANCED},
	 *                 {@link BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER}.
	 * @return The request.
	 */
	@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
	protected ConnectionPriorityRequest requestConnectionPriority(
			@ConnectionPriority final int priority) {
		return Request.newConnectionPriorityRequest(priority).setManager(this);
	}

	@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
	@MainThread
	private boolean internalRequestConnectionPriority(@ConnectionPriority final int priority) {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || !mConnected)
			return false;

		String text, priorityText;
		switch (priority) {
			case ConnectionPriorityRequest.CONNECTION_PRIORITY_HIGH:
				text = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ?
						"HIGH (11.25–15ms, 0, 20s)" : "HIGH (7.5–10ms, 0, 20s)";
				priorityText = "HIGH";
				break;
			case ConnectionPriorityRequest.CONNECTION_PRIORITY_LOW_POWER:
				text = "LOW POWER (100–125ms, 2, 20s)";
				priorityText = "LOW POWER";
				break;
			default:
			case ConnectionPriorityRequest.CONNECTION_PRIORITY_BALANCED:
				text = "BALANCED (30–50ms, 0, 20s)";
				priorityText = "BALANCED";
				break;
		}
		log(Log.VERBOSE, "Requesting connection priority: " + text + "...");
		log(Log.DEBUG, "gatt.requestConnectionPriority(" + priorityText + ")");
		return gatt.requestConnectionPriority(priority);
	}

	/**
	 * Enqueues a request to set the preferred PHY.
	 * <p>
	 * PHY LE 2M and PHY LE Coded are supported only on Android Oreo or newer.
	 * You may safely request other PHYs on older platforms, but  you will get PHY LE 1M
	 * as TX and RX PHY in the callback.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param txPhy      preferred transmitter PHY. Bitwise OR of any of
	 *                   {@link PhyRequest#PHY_LE_1M_MASK}, {@link PhyRequest#PHY_LE_2M_MASK},
	 *                   and {@link PhyRequest#PHY_LE_CODED_MASK}.
	 * @param rxPhy      preferred receiver PHY. Bitwise OR of any of
	 *                   {@link PhyRequest#PHY_LE_1M_MASK}, {@link PhyRequest#PHY_LE_2M_MASK},
	 *                   and {@link PhyRequest#PHY_LE_CODED_MASK}.
	 * @param phyOptions preferred coding to use when transmitting on the LE Coded PHY. Can be one
	 *                   of {@link PhyRequest#PHY_OPTION_NO_PREFERRED},
	 *                   {@link PhyRequest#PHY_OPTION_S2} or {@link PhyRequest#PHY_OPTION_S8}.
	 * @return The request.
	 */
	protected PhyRequest setPreferredPhy(@PhyMask final int txPhy, @PhyMask final int rxPhy,
										 @PhyOption final int phyOptions) {
		return Request.newSetPreferredPhyRequest(txPhy, rxPhy, phyOptions).setManager(this);
	}

	@RequiresApi(api = Build.VERSION_CODES.O)
	@MainThread
	private boolean internalSetPreferredPhy(@PhyMask final int txPhy, @PhyMask final int rxPhy,
											@PhyOption final int phyOptions) {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || !mConnected)
			return false;

		log(Log.VERBOSE, "Requesting preferred PHYs...");
		log(Log.DEBUG, "gatt.setPreferredPhy(" + phyMaskToString(txPhy) + ", "
				+ phyMaskToString(rxPhy) + ", coding option = "
				+ phyCodedOptionToString(phyOptions) + ")");
		gatt.setPreferredPhy(txPhy, rxPhy, phyOptions);
		return true;
	}

	/**
	 * Reads the current PHY for this connections.
	 * <p>
	 * PHY LE 2M and PHY LE Coded are supported only on Android Oreo or newer.
	 * You may safely read PHY on older platforms, but you will get PHY LE 1M as TX and RX PHY
	 * in the callback.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @return The request.
	 */
	protected PhyRequest readPhy() {
		return Request.newReadPhyRequest().setManager(this);
	}

	@RequiresApi(api = Build.VERSION_CODES.O)
	@MainThread
	private boolean internalReadPhy() {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || !mConnected)
			return false;

		log(Log.VERBOSE, "Reading PHY...");
		log(Log.DEBUG, "gatt.readPhy()");
		gatt.readPhy();
		return true;
	}

	/**
	 * Reads the current RSSI (Received Signal Strength Indication).
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @return The request.
	 */
	protected ReadRssiRequest readRssi() {
		return Request.newReadRssiRequest().setManager(this);
	}

	@MainThread
	private boolean internalReadRssi() {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null || !mConnected)
			return false;

		log(Log.VERBOSE, "Reading remote RSSI...");
		log(Log.DEBUG, "gatt.readRemoteRssi()");
		return gatt.readRemoteRssi();
	}

	/**
	 * Refreshes the device cache. As the {@link BluetoothGatt#refresh()} method is not in the
	 * public API (it's hidden, and on Android P it is on a light gray list) it is called
	 * using reflections and may be removed in some future Android release or on some devices.
	 * <p>
	 * There is no callback indicating when the cache has been cleared. This library assumes
	 * some time and waits. After the delay, it will start service discovery and clear the
	 * task queue. When the service discovery finishes, the
	 * {@link BleManager.BleManagerGattCallback#isRequiredServiceSupported(BluetoothGatt)} and
	 * {@link BleManager.BleManagerGattCallback#isOptionalServiceSupported(BluetoothGatt)} will
	 * be called and the initialization will be performed as if the device has just connected.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @return The request.
	 */
	@SuppressWarnings("JavadocReference")
	protected Request refreshDeviceCache() {
		return Request.newRefreshCacheRequest().setManager(this);
	}

	/**
	 * Clears the device cache.
	 */
	@SuppressWarnings("JavaReflectionMemberAccess")
	@MainThread
	private boolean internalRefreshDeviceCache() {
		final BluetoothGatt gatt = mBluetoothGatt;
		if (gatt == null) // no need to be connected
			return false;

		log(Log.VERBOSE, "Refreshing device cache...");
		log(Log.DEBUG, "gatt.refresh() (hidden)");
		/*
		 * There is a refresh() method in BluetoothGatt class but for now it's hidden.
		 * We will call it using reflections.
		 */
		try {
			final Method refresh = gatt.getClass().getMethod("refresh");
			return (Boolean) refresh.invoke(gatt);
		} catch (final Exception e) {
			Log.w(TAG, "An exception occurred while refreshing device", e);
			log(Log.WARN, "gatt.refresh() method not found");
		}
		return false;
	}

	/**
	 * Enqueues a sleep operation with given duration. The next request will be performed after
	 * at least given number of milliseconds.
	 * <p>
	 * The returned request must be either enqueued using {@link Request#enqueue()} for
	 * asynchronous use, or awaited using await() in synchronous execution.
	 *
	 * @param delay the delay in milliseconds.
	 * @return The request.
	 */
	protected SleepRequest sleep(@IntRange(from = 0) final long delay) {
		return Request.newSleepRequest(delay).setManager(this);
	}

	/**
	 * Enqueues a new request.
	 *
	 * @param request the new request to be added to the end of the queue.
	 * @deprecated The access modifier of this method will be changed to package only.
	 */
	@Deprecated
	protected final void enqueue(@NonNull final Request request) {
		BleManagerGattCallback callback = mGattCallback;
		if (callback == null) {
			callback = mGattCallback = getGattCallback();
		}
		// Add the new task to the end of the queue.
		final BleManagerGattCallback finalCallback = callback;
		finalCallback.enqueue(request);
		runOnUiThread(() -> finalCallback.nextRequest(false));
	}

	/**
	 * Cancels all the enqueued operations. The one currently executed will be finished.
	 */
	protected void clearQueue() {
		final BleManagerGattCallback callback = mGattCallback;
		if (callback != null) {
			callback.cancelQueue();
		}
	}

	private void runOnUiThread(final Runnable runnable) {
		if (Looper.myLooper() != Looper.getMainLooper()) {
			mHandler.post(runnable);
		} else {
			runnable.run();
		}
	}

	@MainThread
	@Override
	void onRequestTimeout(@NonNull final TimeoutableRequest request) {
		mRequest = null;
		mValueChangedRequest = null;
		if (request.type == Request.Type.CONNECT) {
			mConnectRequest = null;
			internalDisconnect();
			// The method above will call mGattCallback.nextRequest(true) so we have to return here.
			return;
		}
		if (request.type == Request.Type.DISCONNECT) {
			close();
			return;
		}
		final BleManagerGattCallback callback = mGattCallback;
		if (callback != null) {
			callback.nextRequest(true);
		}
	}

	@SuppressWarnings({"WeakerAccess", "DeprecatedIsStillUsed"})
	protected abstract class BleManagerGattCallback extends MainThreadBluetoothGattCallback {
		private final static String ERROR_CONNECTION_STATE_CHANGE = "Error on connection state change";
		private final static String ERROR_DISCOVERY_SERVICE = "Error on discovering services";
		private final static String ERROR_AUTH_ERROR_WHILE_BONDED = "Phone has lost bonding information";
		private final static String ERROR_READ_CHARACTERISTIC = "Error on reading characteristic";
		private final static String ERROR_WRITE_CHARACTERISTIC = "Error on writing characteristic";
		private final static String ERROR_READ_DESCRIPTOR = "Error on reading descriptor";
		private final static String ERROR_WRITE_DESCRIPTOR = "Error on writing descriptor";
		private final static String ERROR_MTU_REQUEST = "Error on mtu request";
		private final static String ERROR_CONNECTION_PRIORITY_REQUEST = "Error on connection priority request";
		private final static String ERROR_READ_RSSI = "Error on RSSI read";
		private final static String ERROR_READ_PHY = "Error on PHY read";
		private final static String ERROR_PHY_UPDATE = "Error on PHY update";
		private final static String ERROR_RELIABLE_WRITE = "Error on Execute Reliable Write";

		private final Deque<Request> mTaskQueue = new LinkedList<>();
		private Deque<Request> mInitQueue;
		private boolean mInitInProgress;
		private boolean mOperationInProgress;

		/**
		 * This flag is required to resume operations after the connection priority request was made.
		 * It is used only on Android Oreo and newer, as only there there is onConnectionUpdated
		 * callback. However, as this callback is triggered every time the connection parameters
		 * change, even when such request wasn't made, this flag ensures the nextRequest() method
		 * won't be called during another operation.
		 */
		private boolean mConnectionPriorityOperationInProgress = false;

		/**
		 * This method should return <code>true</code> when the gatt device supports the
		 * required services.
		 *
		 * @param gatt the gatt device with services discovered
		 * @return <code>True</code> when the device has the required service.
		 */
		protected abstract boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt);

		/**
		 * This method should return <code>true</code> when the gatt device supports the
		 * optional services. The default implementation returns <code>false</code>.
		 *
		 * @param gatt the gatt device with services discovered
		 * @return <code>True</code> when the device has the optional service.
		 */
		protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) {
			return false;
		}

		/**
		 * This method should return a list of requests needed to initialize the profile.
		 * Enabling Service Change indications for bonded devices and reading the Battery Level
		 * value and enabling Battery Level notifications is handled before executing this queue.
		 * The queue should not have requests that are not available, e.g. should not read an
		 * optional service when it is not supported by the connected device.
		 * <p>
		 * This method is called when the services has been discovered and the device is supported
		 * (has required service).
		 *
		 * @param gatt the gatt device with services discovered
		 * @return The queue of requests.
		 * @deprecated Use {@link #initialize()} instead.
		 */
		@Deprecated
		protected Deque<Request> initGatt(@NonNull final BluetoothGatt gatt) {
			return null;
		}

		/**
		 * This method should set up the request queue needed to initialize the profile.
		 * Enabling Service Change indications for bonded devices is handled before executing this
		 * queue. The queue may have requests that are not available, e.g. read an optional
		 * service when it is not supported by the connected device. Such call will trigger
		 * {@link Request#fail(FailCallback)}.
		 * <p>
		 * This method is called from the main thread when the services has been discovered and
		 * the device is supported (has required service).
		 * <p>
		 * Remember to call {@link Request#enqueue()} for each request.
		 * <p>
		 * A sample initialization should look like this:
		 * <pre>
		 * &#64;Override
		 * protected void initialize() {
		 *    requestMtu(MTU)
		 *       .with((device, mtu) -> {
		 *           ...
		 *       })
		 *       .enqueue();
		 *    setNotificationCallback(characteristic)
		 *       .with((device, data) -> {
		 *           ...
		 *       });
		 *    enableNotifications(characteristic)
		 *       .done(device -> {
		 *           ...
		 *       })
		 *       .fail((device, status) -> {
		 *           ...
		 *       })
		 *       .enqueue();
		 * }
		 * </pre>
		 */
		protected void initialize() {
			// empty initialization queue
		}

		/**
		 * Called then the initialization queue is complete.
		 */
		protected void onDeviceReady() {
			mCallbacks.onDeviceReady(mBluetoothGatt.getDevice());
		}

		/**
		 * Called each time the task queue gets cleared.
		 */
		protected void onManagerReady() {
			// empty
		}

		/**
		 * This method should nullify all services and characteristics of the device.
		 * It's called when the device is no longer connected, either due to user action
		 * or a link loss.
		 */
		protected abstract void onDeviceDisconnected();

		private void notifyDeviceDisconnected(@NonNull final BluetoothDevice device) {
			final boolean wasConnected = mConnected;
			mConnected = false;
			mServicesDiscovered = false;
			mServiceDiscoveryRequested = false;
			mInitInProgress = false;
			mConnectionState = BluetoothGatt.STATE_DISCONNECTED;
			if (!wasConnected) {
				log(Log.WARN, "Connection attempt timed out");
				close();
				mCallbacks.onDeviceDisconnected(device);
				// ConnectRequest was already notified
			} else if (mUserDisconnected) {
				log(Log.INFO, "Disconnected");
				close();
				mCallbacks.onDeviceDisconnected(device);
				final Request request = mRequest;
				if (request != null && request.type == Request.Type.DISCONNECT) {
					request.notifySuccess(device);
				}
			} else {
				log(Log.WARN, "Connection lost");
				mCallbacks.onLinkLossOccurred(device);
				// We are not closing the connection here as the device should try to reconnect
				// automatically.
				// This may be only called when the shouldAutoConnect() method returned true.
			}
			onDeviceDisconnected();
		}

		/**
		 * Callback reporting the result of a characteristic read operation.
		 *
		 * @param gatt           GATT client
		 * @param characteristic Characteristic that was read from the associated remote device.
		 * @deprecated Use {@link ReadRequest#with(DataReceivedCallback)} instead.
		 */
		@Deprecated
		protected void onCharacteristicRead(@NonNull final BluetoothGatt gatt,
											@NonNull final BluetoothGattCharacteristic characteristic) {
			// do nothing
		}

		/**
		 * Callback indicating the result of a characteristic write operation.
		 * <p>If this callback is invoked while a reliable write transaction is
		 * in progress, the value of the characteristic represents the value
		 * reported by the remote device. An application should compare this
		 * value to the desired value to be written. If the values don't match,
		 * the application must abort the reliable write transaction.
		 *
		 * @param gatt           GATT client
		 * @param characteristic Characteristic that was written to the associated remote device.
		 * @deprecated Use {@link WriteRequest#done(SuccessCallback)} instead.
		 */
		@Deprecated
		protected void onCharacteristicWrite(@NonNull final BluetoothGatt gatt,
											 @NonNull final BluetoothGattCharacteristic characteristic) {
			// do nothing
		}

		/**
		 * Callback reporting the result of a descriptor read operation.
		 *
		 * @param gatt       GATT client
		 * @param descriptor Descriptor that was read from the associated remote device.
		 * @deprecated Use {@link ReadRequest#with(DataReceivedCallback)} instead.
		 */
		@Deprecated
		protected void onDescriptorRead(@NonNull final BluetoothGatt gatt,
										@NonNull final BluetoothGattDescriptor descriptor) {
			// do nothing
		}

		/**
		 * Callback indicating the result of a descriptor write operation.
		 * <p>If this callback is invoked while a reliable write transaction is in progress,
		 * the value of the characteristic represents the value reported by the remote device.
		 * An application should compare this value to the desired value to be written.
		 * If the values don't match, the application must abort the reliable write transaction.
		 *
		 * @param gatt       GATT client
		 * @param descriptor Descriptor that was written to the associated remote device.
		 * @deprecated Use {@link WriteRequest} and {@link SuccessCallback} instead.
		 */
		@Deprecated
		protected void onDescriptorWrite(@NonNull final BluetoothGatt gatt,
										 @NonNull final BluetoothGattDescriptor descriptor) {
			// do nothing
		}

		/**
		 * Callback reporting the value of Battery Level characteristic which could have
		 * been received by Read or Notify operations.
		 * <p>
		 * This method will not be called if {@link #readBatteryLevel()} and
		 * {@link #enableBatteryLevelNotifications()} were overridden.
		 * </p>
		 *
		 * @param gatt  GATT client
		 * @param value the battery value in percent
		 * @deprecated Use {@link ReadRequest#with(DataReceivedCallback)} and
		 * BatteryLevelDataCallback from BLE-Common-Library instead.
		 */
		@Deprecated
		protected void onBatteryValueReceived(@NonNull final BluetoothGatt gatt,
											  @IntRange(from = 0, to = 100) final int value) {
			// do nothing
		}

		/**
		 * Callback indicating a notification has been received.
		 *
		 * @param gatt           GATT client
		 * @param characteristic Characteristic from which the notification came.
		 * @deprecated Use {@link ReadRequest#with(DataReceivedCallback)} instead.
		 */
		@Deprecated
		protected void onCharacteristicNotified(@NonNull final BluetoothGatt gatt,
												@NonNull final BluetoothGattCharacteristic characteristic) {
			// do nothing
		}

		/**
		 * Callback indicating an indication has been received.
		 *
		 * @param gatt           GATT client
		 * @param characteristic Characteristic from which the indication came.
		 * @deprecated Use {@link ReadRequest#with(DataReceivedCallback)} instead.
		 */
		@Deprecated
		protected void onCharacteristicIndicated(@NonNull final BluetoothGatt gatt,
												 @NonNull final BluetoothGattCharacteristic characteristic) {
			// do nothing
		}

		/**
		 * Method called when the MTU request has finished with success. The MTU value may
		 * be different than requested one.
		 *
		 * @param gatt GATT client
		 * @param mtu  the new MTU (Maximum Transfer Unit)
		 * @deprecated Use {@link MtuRequest#with(MtuCallback)} instead.
		 */
		@Deprecated
		protected void onMtuChanged(@NonNull final BluetoothGatt gatt,
									@IntRange(from = 23, to = 517) final int mtu) {
			// do nothing
		}

		/**
		 * Callback indicating the connection parameters were updated. Works on Android 8+.
		 *
		 * @param gatt     GATT client.
		 * @param interval Connection interval used on this connection, 1.25ms unit.
		 *                 Valid range is from 6 (7.5ms) to 3200 (4000ms).
		 * @param latency  Slave latency for the connection in number of connection events.
		 *                 Valid range is from 0 to 499.
		 * @param timeout  Supervision timeout for this connection, in 10ms unit.
		 *                 Valid range is from 10 (0.1s) to 3200 (32s).
		 * @deprecated Use {@link ConnectionPriorityRequest#with(ConnectionPriorityCallback)} instead.
		 */
		@Deprecated
		@TargetApi(Build.VERSION_CODES.O)
		protected void onConnectionUpdated(@NonNull final BluetoothGatt gatt,
										   @IntRange(from = 6, to = 3200) final int interval,
										   @IntRange(from = 0, to = 499) final int latency,
										   @IntRange(from = 10, to = 3200) final int timeout) {
			// do nothing
		}

		private void onError(final BluetoothDevice device, final String message, final int errorCode) {
			log(Log.ERROR, "Error (0x" + Integer.toHexString(errorCode) + "): "
					+ GattError.parse(errorCode));
			mCallbacks.onError(device, message, errorCode);
		}

		@Override
		final void onConnectionStateChangeSafe(@NonNull final BluetoothGatt gatt, final int status, final int newState) {
			log(Log.DEBUG, "[Callback] Connection state changed with status: " +
					status + " and new state: " + newState + " (" + stateToString(newState) + ")");

			if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
				// Sometimes, when a notification/indication is received after the device got
				// disconnected, the Android calls onConnectionStateChanged again, with state
				// STATE_CONNECTED.
				// See: https://github.com/NordicSemiconductor/Android-BLE-Library/issues/43
				if (mBluetoothDevice == null) {
					Log.e(TAG, "Device received notification after disconnection.");
					log(Log.DEBUG, "gatt.close()");
					gatt.close();
					return;
				}

				// Notify the parent activity/service.
				log(Log.INFO, "Connected to " + gatt.getDevice().getAddress());
				mConnected = true;
				mConnectionTime = 0L;
				mConnectionState = BluetoothGatt.STATE_CONNECTED;
				mCallbacks.onDeviceConnected(gatt.getDevice());

				if (!mServiceDiscoveryRequested) {
					final boolean bonded = gatt.getDevice().getBondState() == BluetoothDevice.BOND_BONDED;
					final int delay = getServiceDiscoveryDelay(bonded);
					if (delay > 0)
						log(Log.DEBUG, "wait(" + delay + ")");

					final int connectionCount = ++mConnectionCount;
					mHandler.postDelayed(() -> {
						if (connectionCount != mConnectionCount) {
							// Ensure that we will not try to discover services for a lost connection.
							return;
						}
						// Some proximity tags (e.g. nRF PROXIMITY Pebble) initialize bonding
						// automatically when connected. Wait with the discovery until bonding is
						// complete. It will be initiated again in the bond state broadcast receiver
						// on the top of this file.
						if (mConnected &&
								gatt.getDevice().getBondState() != BluetoothDevice.BOND_BONDING) {
							mServiceDiscoveryRequested = true;
							log(Log.VERBOSE, "Discovering services...");
							log(Log.DEBUG, "gatt.discoverServices()");
							gatt.discoverServices();
						}
					}, delay);
				}
			} else {
				if (newState == BluetoothProfile.STATE_DISCONNECTED) {
					final long now = SystemClock.elapsedRealtime();
					final boolean canTimeout = mConnectionTime > 0;
					final boolean timeout = canTimeout && now > mConnectionTime + CONNECTION_TIMEOUT_THRESHOLD;

					if (status != BluetoothGatt.GATT_SUCCESS)
						log(Log.WARN, "Error: (0x" + Integer.toHexString(status) + "): " +
								GattError.parseConnectionError(status));

					// In case of a connection error, retry if required.
					if (status != BluetoothGatt.GATT_SUCCESS && canTimeout && !timeout
							&& mConnectRequest != null && mConnectRequest.canRetry()) {
						final int delay = mConnectRequest.getRetryDelay();
						if (delay > 0)
							log(Log.DEBUG, "wait(" + delay + ")");
						mHandler.postDelayed(() -> internalConnect(gatt.getDevice(), mConnectRequest), delay);
						return;
					}

					mOperationInProgress = true; // no more calls are possible
					cancelQueue();
					mInitQueue = null;
					mReady = false;

					// Store the current value of the mConnected flag...
					final boolean wasConnected = mConnected;
					// ...because this method sets the mConnected flag to false.
					notifyDeviceDisconnected(gatt.getDevice()); // this may call close()

					// Signal the current request, if any.
					if (mRequest != null) {
						if (mRequest.type != Request.Type.DISCONNECT && mRequest.type != Request.Type.REMOVE_BOND) {
							// The CONNECT request is notified below.
							// The DISCONNECT request is notified below in
							// notifyDeviceDisconnected(BluetoothDevice).
							// The REMOVE_BOND request will be notified when the bond state changes
							// to BOND_NONE in the broadcast received on the top of this file.
							mRequest.notifyFail(gatt.getDevice(),
									status == BluetoothGatt.GATT_SUCCESS ?
											FailCallback.REASON_DEVICE_DISCONNECTED : status);
							mRequest = null;
						}
					}
					if (mValueChangedRequest != null) {
						mValueChangedRequest.notifyFail(mBluetoothDevice, FailCallback.REASON_DEVICE_DISCONNECTED);
						mValueChangedRequest = null;
					}
					if (mConnectRequest != null) {
						int reason;
						if (mServicesDiscovered)
							reason = FailCallback.REASON_DEVICE_NOT_SUPPORTED;
						else if (status == BluetoothGatt.GATT_SUCCESS)
							reason = FailCallback.REASON_DEVICE_DISCONNECTED;
						else if (status == GattError.GATT_ERROR && timeout)
							reason = FailCallback.REASON_TIMEOUT;
						else
							reason = status;
						mConnectRequest.notifyFail(gatt.getDevice(), reason);
						mConnectRequest = null;
					}

					// Reset flag, so the next Connect could be enqueued.
					mOperationInProgress = false;
					// Try to reconnect if the initial connection was lost because of a link loss,
					// and shouldAutoConnect() returned true during connection attempt.
					// This time it will set the autoConnect flag to true (gatt.connect() forces
					// autoConnect true).
					if (wasConnected && mInitialConnection) {
						internalConnect(gatt.getDevice(), null);
					} else {
						mInitialConnection = false;
						nextRequest(false);
					}

					if (wasConnected || status == BluetoothGatt.GATT_SUCCESS)
						return;
				} else {
					if (status != BluetoothGatt.GATT_SUCCESS)
						log(Log.ERROR, "Error (0x" + Integer.toHexString(status) + "): " +
								GattError.parseConnectionError(status));
				}
				mCallbacks.onError(gatt.getDevice(), ERROR_CONNECTION_STATE_CHANGE, status);
			}
		}

		@Override
		final void onServicesDiscoveredSafe(@NonNull final BluetoothGatt gatt, final int status) {
			mServiceDiscoveryRequested = false;
			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "Services discovered");
				mServicesDiscovered = true;
				if (isRequiredServiceSupported(gatt)) {
					log(Log.VERBOSE, "Primary service found");
					final boolean optionalServicesFound = isOptionalServiceSupported(gatt);
					if (optionalServicesFound)
						log(Log.VERBOSE, "Secondary service found");

					// Notify the parent activity.
					mCallbacks.onServicesDiscovered(gatt.getDevice(), optionalServicesFound);

					// Obtain the queue of initialization requests.
					// First, let's call the deprecated initGatt(...).
					mInitInProgress = true;
					mOperationInProgress = true;
					mInitQueue = initGatt(gatt);

					final boolean deprecatedApiUsed = mInitQueue != null;
					if (deprecatedApiUsed) {
						for (final Request request : mInitQueue) {
							request.enqueued = true;
						}
					}

					if (mInitQueue == null)
						mInitQueue = new LinkedList<>();

					// Before we start executing the initialization queue some other tasks
					// need to be done.
					// Note, that operations are added in reverse order to the front of the queue.

					// 1. On devices running Android 4.3-5.x, 8.x and 9.0 the Service Changed
					//    characteristic needs to be enabled by the app (for bonded devices).
					//    The request will be ignored if there is no Service Changed characteristic.
					// This "fix" broke this in Android 8:
					// https://android-review.googlesource.com/c/platform/system/bt/+/239970
					if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
							|| Build.VERSION.SDK_INT == Build.VERSION_CODES.O
							|| Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1
							|| Build.VERSION.SDK_INT == Build.VERSION_CODES.P)
						enqueueFirst(Request.newEnableServiceChangedIndicationsRequest()
								.setManager(BleManager.this));

					// Deprecated:
					if (deprecatedApiUsed) {
						// All Battery Service handling will be removed from BleManager in the future.
						// If you want to read/enable notifications on Battery Level characteristic
						// do this in initialize(...).

						// 2. Read Battery Level characteristic (if such does not exist, this will
						//    be skipped)
						readBatteryLevel();
						// 3. Enable Battery Level notifications if required (if this char. does not
						//    exist, this operation will be skipped)
						if (mCallbacks.shouldEnableBatteryLevelNotifications(gatt.getDevice()))
							enableBatteryLevelNotifications();
					}
					// End

					initialize();
					mInitInProgress = false;
					nextRequest(true);
				} else {
					log(Log.WARN, "Device is not supported");
					mCallbacks.onDeviceNotSupported(gatt.getDevice());
					internalDisconnect();
				}
			} else {
				Log.e(TAG, "onServicesDiscovered error " + status);
				onError(gatt.getDevice(), ERROR_DISCOVERY_SERVICE, status);
				if (mConnectRequest != null) {
					mConnectRequest.notifyFail(gatt.getDevice(), FailCallback.REASON_REQUEST_FAILED);
					mConnectRequest = null;
				}
				internalDisconnect();
			}
		}

		@Override
		final void onCharacteristicReadSafe(@NonNull final BluetoothGatt gatt,
											@NonNull final BluetoothGattCharacteristic characteristic,
											@Nullable final byte[] data, final int status) {
			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "Read Response received from " + characteristic.getUuid() +
						", value: " + ParserUtils.parse(data));

				onCharacteristicRead(gatt, characteristic);
				if (mRequest instanceof ReadRequest) {
					final ReadRequest request = (ReadRequest) mRequest;
					final boolean matches = request.matches(data);
					if (matches) {
						request.notifyValueChanged(gatt.getDevice(), data);
					}
					if (!matches || request.hasMore()) {
						enqueueFirst(request);
					} else {
						request.notifySuccess(gatt.getDevice());
					}
				}
			} else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION
					|| status == 8 /* GATT INSUF AUTHORIZATION */
					|| status == 137 /* GATT AUTH FAIL */) {
				log(Log.WARN, "Authentication required (" + status + ")");
				if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) {
					// This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352
					Log.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED);
					mCallbacks.onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status);
				}
				// The request will be repeated when the bond state changes to BONDED.
				return;
			} else {
				Log.e(TAG, "onCharacteristicRead error " + status);
				if (mRequest instanceof ReadRequest) {
					mRequest.notifyFail(gatt.getDevice(), status);
				}
				mValueChangedRequest = null;
				onError(gatt.getDevice(), ERROR_READ_CHARACTERISTIC, status);
			}
			nextRequest(true);
		}

		@Override
		final void onCharacteristicWriteSafe(@NonNull final BluetoothGatt gatt,
											 @NonNull final BluetoothGattCharacteristic characteristic,
											 @Nullable final byte[] data, final int status) {
			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "Data written to " + characteristic.getUuid() +
						", value: " + ParserUtils.parse(data));

				onCharacteristicWrite(gatt, characteristic);
				if (mRequest instanceof WriteRequest) {
					final WriteRequest request = (WriteRequest) mRequest;
					final boolean valid = request.notifyPacketSent(gatt.getDevice(), data);
					if (!valid && mRequestQueue instanceof ReliableWriteRequest) {
						request.notifyFail(gatt.getDevice(), FailCallback.REASON_VALIDATION);
						mRequestQueue.cancelQueue();
					} else if (request.hasMore()) {
						enqueueFirst(request);
					} else {
						request.notifySuccess(gatt.getDevice());
					}
				}
			} else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION
					|| status == 8 /* GATT INSUF AUTHORIZATION */
					|| status == 137 /* GATT AUTH FAIL */) {
				log(Log.WARN, "Authentication required (" + status + ")");
				if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) {
					// This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352
					Log.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED);
					mCallbacks.onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status);
				}
				// The request will be repeated when the bond state changes to BONDED.
				return;
			} else {
				Log.e(TAG, "onCharacteristicWrite error " + status);
				if (mRequest instanceof WriteRequest) {
					mRequest.notifyFail(gatt.getDevice(), status);
					// Automatically abort Reliable Write when write error happen
					if (mRequestQueue instanceof ReliableWriteRequest)
						mRequestQueue.cancelQueue();
				}
				mValueChangedRequest = null;
				onError(gatt.getDevice(), ERROR_WRITE_CHARACTERISTIC, status);
			}
			nextRequest(true);
		}

		@Override
		final void onReliableWriteCompletedSafe(@NonNull final BluetoothGatt gatt,
												final int status) {
			final boolean execute = mRequest.type == Request.Type.EXECUTE_RELIABLE_WRITE;
			mReliableWriteInProgress = false;
			if (status == BluetoothGatt.GATT_SUCCESS) {
				if (execute) {
					log(Log.INFO, "Reliable Write executed");
					mRequest.notifySuccess(gatt.getDevice());
				} else {
					log(Log.WARN, "Reliable Write aborted");
					mRequest.notifySuccess(gatt.getDevice());
					mRequestQueue.notifyFail(gatt.getDevice(), FailCallback.REASON_REQUEST_FAILED);
				}
			} else {
				Log.e(TAG, "onReliableWriteCompleted execute " + execute + ", error " + status);
				mRequest.notifyFail(gatt.getDevice(), status);
				onError(gatt.getDevice(), ERROR_RELIABLE_WRITE, status);
			}
			nextRequest(true);
		}

		@Override
		void onDescriptorReadSafe(@NonNull final BluetoothGatt gatt,
								  @NonNull final BluetoothGattDescriptor descriptor,
								  @Nullable final byte[] data, final int status) {
			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "Read Response received from descr. " + descriptor.getUuid() +
						", value: " + ParserUtils.parse(data));

				onDescriptorRead(gatt, descriptor);
				if (mRequest instanceof ReadRequest) {
					final ReadRequest request = (ReadRequest) mRequest;
					request.notifyValueChanged(gatt.getDevice(), data);
					if (request.hasMore()) {
						enqueueFirst(request);
					} else {
						request.notifySuccess(gatt.getDevice());
					}
				}
			} else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION
					|| status == 8 /* GATT INSUF AUTHORIZATION */
					|| status == 137 /* GATT AUTH FAIL */) {
				log(Log.WARN, "Authentication required (" + status + ")");
				if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) {
					// This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352
					Log.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED);
					mCallbacks.onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status);
				}
				// The request will be repeated when the bond state changes to BONDED.
				return;
			} else {
				Log.e(TAG, "onDescriptorRead error " + status);
				if (mRequest instanceof ReadRequest) {
					mRequest.notifyFail(gatt.getDevice(), status);
				}
				mValueChangedRequest = null;
				onError(gatt.getDevice(), ERROR_READ_DESCRIPTOR, status);
			}
			nextRequest(true);
		}

		@Override
		final void onDescriptorWriteSafe(@NonNull final BluetoothGatt gatt,
										 @NonNull final BluetoothGattDescriptor descriptor,
										 @Nullable final byte[] data, final int status) {
			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "Data written to descr. " + descriptor.getUuid() +
						", value: " + ParserUtils.parse(data));

				if (isServiceChangedCCCD(descriptor)) {
					log(Log.INFO, "Service Changed notifications enabled");
				} else if (isCCCD(descriptor)) {
					if (data != null && data.length == 2 && data[1] == 0x00) {
						switch (data[0]) {
							case 0x00:
								mNotificationCallbacks.remove(descriptor.getCharacteristic());
								log(Log.INFO, "Notifications and indications disabled");
								break;
							case 0x01:
								log(Log.INFO, "Notifications enabled");
								break;
							case 0x02:
								log(Log.INFO, "Indications enabled");
								break;
						}
						onDescriptorWrite(gatt, descriptor);
					}
				} else {
					onDescriptorWrite(gatt, descriptor);
				}
				if (mRequest instanceof WriteRequest) {
					final WriteRequest request = (WriteRequest) mRequest;
					final boolean valid = request.notifyPacketSent(gatt.getDevice(), data);
					if (!valid && mRequestQueue instanceof ReliableWriteRequest) {
						request.notifyFail(gatt.getDevice(), FailCallback.REASON_VALIDATION);
						mRequestQueue.cancelQueue();
					} else if (request.hasMore()) {
						enqueueFirst(request);
					} else {
						request.notifySuccess(gatt.getDevice());
					}
				}
			} else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION
					|| status == 8 /* GATT INSUF AUTHORIZATION */
					|| status == 137 /* GATT AUTH FAIL */) {
				log(Log.WARN, "Authentication required (" + status + ")");
				if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) {
					// This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352
					Log.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED);
					mCallbacks.onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status);
				}
				// The request will be repeated when the bond state changes to BONDED.
				return;
			} else {
				Log.e(TAG, "onDescriptorWrite error " + status);
				if (mRequest instanceof WriteRequest) {
					mRequest.notifyFail(gatt.getDevice(), status);
					// Automatically abort Reliable Write when write error happen
					if (mRequestQueue instanceof ReliableWriteRequest)
						mRequestQueue.cancelQueue();
				}
				mValueChangedRequest = null;
				onError(gatt.getDevice(), ERROR_WRITE_DESCRIPTOR, status);
			}
			nextRequest(true);
		}

		@Override
		final void onCharacteristicChangedSafe(@NonNull final BluetoothGatt gatt,
											   @NonNull final BluetoothGattCharacteristic characteristic,
											   @Nullable final byte[] data) {
			if (isServiceChangedCharacteristic(characteristic)) {
				// TODO this should be tested. Should services be invalidated?
				// Forbid enqueuing more operations.
				mOperationInProgress = true;
				// Clear queues, services are no longer valid.
				cancelQueue();
				mInitQueue = null;
				log(Log.INFO, "Service Changed indication received");
				log(Log.VERBOSE, "Discovering Services...");
				log(Log.DEBUG, "gatt.discoverServices()");
				gatt.discoverServices();
			} else {
				final BluetoothGattDescriptor cccd =
						characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID);
				final boolean notifications = cccd == null || cccd.getValue() == null ||
						cccd.getValue().length != 2 || cccd.getValue()[0] == 0x01;

				final String dataString = ParserUtils.parse(data);
				if (notifications) {
					log(Log.INFO, "Notification received from " +
							characteristic.getUuid() + ", value: " + dataString);
					onCharacteristicNotified(gatt, characteristic);
				} else { // indications
					log(Log.INFO, "Indication received from " +
							characteristic.getUuid() + ", value: " + dataString);
					onCharacteristicIndicated(gatt, characteristic);
				}
				if (mBatteryLevelNotificationCallback != null && isBatteryLevelCharacteristic(characteristic)) {
					mBatteryLevelNotificationCallback.notifyValueChanged(gatt.getDevice(), data);
				}
				// Notify the notification registered listener, if set
				final ValueChangedCallback request = mNotificationCallbacks.get(characteristic);
				if (request != null && request.matches(data)) {
					request.notifyValueChanged(gatt.getDevice(), data);
				}
				// If there is a value change request,
				final WaitForValueChangedRequest valueChangedRequest = mValueChangedRequest;
				if (valueChangedRequest != null
						// registered for this characteristic
						&& valueChangedRequest.characteristic == characteristic
						// and didn't have a trigger, or the trigger was started
						// (not necessarily completed)
						&& !valueChangedRequest.isTriggerPending()
						// and the data matches the filter (if set)
						&& valueChangedRequest.matches(data)) {
					// notify that new data was received.
					valueChangedRequest.notifyValueChanged(gatt.getDevice(), data);

					// If no more data are expected
					if (!valueChangedRequest.hasMore()) {
						// notify success,
						valueChangedRequest.notifySuccess(gatt.getDevice());
						// and proceed to the next request only if the trigger has completed.
						// Otherwise, the next request will be started when the request's callback
						// will be received.
						mValueChangedRequest = null;
						if (valueChangedRequest.isTriggerCompleteOrNull()) {
							nextRequest(true);
						}
					}
				}
			}
		}

		@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
		@Override
		final void onMtuChangedSafe(@NonNull final BluetoothGatt gatt,
									@IntRange(from = 23, to = 517) final int mtu,
									final int status) {
			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "MTU changed to: " + mtu);
				mMtu = mtu;
				onMtuChanged(gatt, mtu);
				if (mRequest instanceof MtuRequest) {
					((MtuRequest) mRequest).notifyMtuChanged(gatt.getDevice(), mtu);
					mRequest.notifySuccess(gatt.getDevice());
				}
			} else {
				Log.e(TAG, "onMtuChanged error: " + status + ", mtu: " + mtu);
				if (mRequest instanceof MtuRequest) {
					mRequest.notifyFail(gatt.getDevice(), status);
					mValueChangedRequest = null;
				}
				onError(gatt.getDevice(), ERROR_MTU_REQUEST, status);
			}
			nextRequest(true);
		}

		/**
		 * Callback indicating the connection parameters were updated. Works on Android 8+.
		 *
		 * @param gatt     GATT client involved.
		 * @param interval Connection interval used on this connection, 1.25ms unit.
		 *                 Valid range is from 6 (7.5ms) to 3200 (4000ms).
		 * @param latency  Slave latency for the connection in number of connection events.
		 *                 Valid range is from 0 to 499.
		 * @param timeout  Supervision timeout for this connection, in 10ms unit.
		 *                 Valid range is from 10 (0.1s) to 3200 (32s)
		 * @param status   {@link BluetoothGatt#GATT_SUCCESS} if the connection has been updated
		 *                 successfully.
		 */
		@RequiresApi(api = Build.VERSION_CODES.O)
		@Override
		final void onConnectionUpdatedSafe(@NonNull final BluetoothGatt gatt,
										   @IntRange(from = 6, to = 3200) final int interval,
										   @IntRange(from = 0, to = 499) final int latency,
										   @IntRange(from = 10, to = 3200) final int timeout,
										   final int status) {
			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "Connection parameters updated " +
						"(interval: " + (interval * 1.25) + "ms," +
						" latency: " + latency + ", timeout: " + (timeout * 10) + "ms)");
				onConnectionUpdated(gatt, interval, latency, timeout);

				// This callback may be called af any time, also when some other request is executed
				if (mRequest instanceof ConnectionPriorityRequest) {
					((ConnectionPriorityRequest) mRequest)
							.notifyConnectionPriorityChanged(gatt.getDevice(), interval, latency, timeout);
					mRequest.notifySuccess(gatt.getDevice());
				}
			} else if (status == 0x3b) { // HCI_ERR_UNACCEPT_CONN_INTERVAL
				Log.e(TAG, "onConnectionUpdated received status: Unacceptable connection interval, " +
						"interval: " + interval + ", latency: " + latency + ", timeout: " + timeout);
				log(Log.WARN, "Connection parameters update failed with status: " +
						"UNACCEPT CONN INTERVAL (0x3b) (interval: " + (interval * 1.25) + "ms, " +
						"latency: " + latency + ", timeout: " + (timeout * 10) + "ms)");

				// This callback may be called af any time, also when some other request is executed
				if (mRequest instanceof ConnectionPriorityRequest) {
					mRequest.notifyFail(gatt.getDevice(), status);
					mValueChangedRequest = null;
				}
			} else {
				Log.e(TAG, "onConnectionUpdated received status: " + status + ", " +
						"interval: " + interval + ", latency: " + latency + ", timeout: " + timeout);
				log(Log.WARN, "Connection parameters update failed with " +
						"status " + status + " (interval: " + (interval * 1.25) + "ms, " +
						"latency: " + latency + ", timeout: " + (timeout * 10) + "ms)");

				// This callback may be called af any time, also when some other request is executed
				if (mRequest instanceof ConnectionPriorityRequest) {
					mRequest.notifyFail(gatt.getDevice(), status);
					mValueChangedRequest = null;
				}
				mCallbacks.onError(gatt.getDevice(), ERROR_CONNECTION_PRIORITY_REQUEST, status);
			}
			if (mConnectionPriorityOperationInProgress) {
				mConnectionPriorityOperationInProgress = false;
				nextRequest(true);
			}
		}

		@RequiresApi(api = Build.VERSION_CODES.O)
		@Override
		final void onPhyUpdateSafe(@NonNull final BluetoothGatt gatt,
								   @PhyValue final int txPhy, @PhyValue final int rxPhy,
								   final int status) {
			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "PHY updated (TX: " + phyToString(txPhy) +
						", RX: " + phyToString(rxPhy) + ")");
				if (mRequest instanceof PhyRequest) {
					((PhyRequest) mRequest).notifyPhyChanged(gatt.getDevice(), txPhy, rxPhy);
					mRequest.notifySuccess(gatt.getDevice());
				}
			} else {
				log(Log.WARN, "PHY updated failed with status " + status);
				if (mRequest instanceof PhyRequest) {
					mRequest.notifyFail(gatt.getDevice(), status);
					mValueChangedRequest = null;
				}
				mCallbacks.onError(gatt.getDevice(), ERROR_PHY_UPDATE, status);
			}
			// PHY update may be requested by the other side, or the Android, without explicitly
			// requesting it. Proceed with the queue only when update was requested.
			if (mRequest instanceof PhyRequest) {
				nextRequest(true);
			}
		}

		@RequiresApi(api = Build.VERSION_CODES.O)
		@Override
		final void onPhyReadSafe(@NonNull final BluetoothGatt gatt,
								 @PhyValue final int txPhy, @PhyValue final int rxPhy,
								 final int status) {
			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "PHY read (TX: " + phyToString(txPhy) +
						", RX: " + phyToString(rxPhy) + ")");
				if (mRequest instanceof PhyRequest) {
					((PhyRequest) mRequest).notifyPhyChanged(gatt.getDevice(), txPhy, rxPhy);
					mRequest.notifySuccess(gatt.getDevice());
				}
			} else {
				log(Log.WARN, "PHY read failed with status " + status);
				if (mRequest instanceof PhyRequest) {
					mRequest.notifyFail(gatt.getDevice(), status);
				}
				mValueChangedRequest = null;
				mCallbacks.onError(gatt.getDevice(), ERROR_READ_PHY, status);
			}
			nextRequest(true);
		}

		@Override
		final void onReadRemoteRssiSafe(@NonNull final BluetoothGatt gatt,
										@IntRange(from = -128, to = 20) final int rssi,
										final int status) {
			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "Remote RSSI received: " + rssi + " dBm");
				if (mRequest instanceof ReadRssiRequest) {
					((ReadRssiRequest) mRequest).notifyRssiRead(gatt.getDevice(), rssi);
					mRequest.notifySuccess(gatt.getDevice());
				}
			} else {
				log(Log.WARN, "Reading remote RSSI failed with status " + status);
				if (mRequest instanceof ReadRssiRequest) {
					mRequest.notifyFail(gatt.getDevice(), status);
				}
				mValueChangedRequest = null;
				mCallbacks.onError(gatt.getDevice(), ERROR_READ_RSSI, status);
			}
			nextRequest(true);
		}

		/**
		 * Enqueues the given request at the front of the the init or task queue, depending
		 * on whether the initialization is in progress, or not.
		 *
		 * @param request the request to be added.
		 */
		private void enqueueFirst(@NonNull final Request request) {
			final Deque<Request> queue = mInitInProgress ? mInitQueue : mTaskQueue;
			queue.addFirst(request);
			request.enqueued = true;
		}

		/**
		 * Enqueues the given request at the end of the the init or task queue, depending
		 * on whether the initialization is in progress, or not.
		 *
		 * @param request the request to be added.
		 */
		private void enqueue(@NonNull final Request request) {
			final Deque<Request> queue = mInitInProgress ? mInitQueue : mTaskQueue;
			queue.add(request);
			request.enqueued = true;
		}

		/**
		 * Removes all enqueued requests from the queue.
		 */
		protected void cancelQueue() {
			mTaskQueue.clear();
		}

		/**
		 * Executes the next request. If the last element from the initialization queue has
		 * been executed the {@link #onDeviceReady()} callback is called.
		 */
		@SuppressWarnings("ConstantConditions")
		@MainThread
		private synchronized void nextRequest(final boolean force) {
			// Is the manager closed()?
			if (mGattCallback == null) {
				return;
			}

			if (force) {
				mOperationInProgress = mValueChangedRequest != null;
			}

			if (mOperationInProgress) {
				return;
			}

			// Get the first request from the init queue
			Request request = null;
			try {
				// If Request set is present, try taking next request from it
				if (mRequestQueue != null) {
					if (mRequestQueue.hasMore()) {
						request = mRequestQueue.getNext().setManager(BleManager.this);
					} else {
						// Set is completed
						mRequestQueue.notifySuccess(mBluetoothDevice);
						mRequestQueue = null;
					}
				}
				// Request wasn't obtained from the request set? Take next one from the queue.
				if (request == null) {
					request = mInitQueue != null ? mInitQueue.poll() : null;
				}
			} catch (final Exception e) {
				// On older Android versions poll() may in some cases throw NoSuchElementException,
				// as it's using removeFirst() internally.
				// See: https://github.com/NordicSemiconductor/Android-BLE-Library/issues/37
				request = null;
			}

			// Are we done with initializing?
			if (request == null) {
				if (mInitQueue != null) {
					mInitQueue = null; // release the queue

					// Set the 'operation in progress' flag, so any request made in onDeviceReady()
					// will not start new nextRequest() call.
					mOperationInProgress = true;
					mReady = true;
					onDeviceReady();
					if (mConnectRequest != null) {
						mConnectRequest.notifySuccess(mConnectRequest.getDevice());
						mConnectRequest = null;
					}
				}
				// If so, we can continue with the task queue
				try {
					request = mTaskQueue.remove();
				} catch (final Exception e) {
					// No more tasks to perform
					mOperationInProgress = false;
					mRequest = null;
					onManagerReady();
					return;
				}
			}

			boolean result = false;
			mOperationInProgress = true;
			mRequest = request;

			// The WAIT_FOR_* request types may override the request with a trigger.
			// This is to ensure that the trigger is done after the mValueChangedRequest was set.
			int requiredProperty = BluetoothGattCharacteristic.PROPERTY_NOTIFY;
			switch (request.type) {
				case WAIT_FOR_INDICATION:
					requiredProperty = BluetoothGattCharacteristic.PROPERTY_INDICATE;
					// fall-through intended
				case WAIT_FOR_NOTIFICATION: {
					final WaitForValueChangedRequest r = (WaitForValueChangedRequest) request;
					result = mConnected && mBluetoothDevice != null
							&& getCccd(r.characteristic, requiredProperty) != null;
					if (result) {
						mValueChangedRequest = r;
						// Call notifyStarted for the WaitForValueChangedRequest.
						r.notifyStarted(mBluetoothDevice);

						if (r.getTrigger() == null)
							break;

						// If the request has another request set as a trigger, update the
						// request with the trigger.
						request = r.getTrigger();
						mRequest = request;
					}
					// fall-through intended
				}
				default: {
					// Call notifyStarted on the request before it's executed.
					if (request.type == Request.Type.CONNECT) {
						// When the Connect Request is started, the mBluetoothDevice is not set yet.
						// It may also be a connect request to a different device, which is an error
						// that is handled in internalConnect()
						final ConnectRequest cr = (ConnectRequest) request;
						cr.notifyStarted(cr.getDevice());
					} else {
						if (mBluetoothDevice != null) {
							request.notifyStarted(mBluetoothDevice);
						} else {
							// The device wasn't connected before. Target is unknown.
							request.notifyInvalidRequest();

							mValueChangedRequest = null;
							nextRequest(true);
							return;
						}
					}
				}
			}

			switch (request.type) {
				case CONNECT: {
					final ConnectRequest cr = (ConnectRequest) request;
					mConnectRequest = cr;
					mRequest = null;
					result = internalConnect(cr.getDevice(), cr);
					break;
				}
				case DISCONNECT: {
					result = internalDisconnect();
					break;
				}
				case CREATE_BOND: {
					result = internalCreateBond();
					break;
				}
				case REMOVE_BOND: {
					result = internalRemoveBond();
					break;
				}
				case SET: {
					mRequestQueue = (RequestQueue) request;
					nextRequest(true);
					return;
				}
				case READ: {
					result = internalReadCharacteristic(request.characteristic);
					break;
				}
				case WRITE: {
					final WriteRequest wr = (WriteRequest) request;
					final BluetoothGattCharacteristic characteristic = request.characteristic;
					if (characteristic != null) {
						characteristic.setValue(wr.getData(mMtu));
						characteristic.setWriteType(wr.getWriteType());
					}
					result = internalWriteCharacteristic(characteristic);
					break;
				}
				case READ_DESCRIPTOR: {
					result = internalReadDescriptor(request.descriptor);
					break;
				}
				case WRITE_DESCRIPTOR: {
					final WriteRequest wr = (WriteRequest) request;
					final BluetoothGattDescriptor descriptor = request.descriptor;
					if (descriptor != null) {
						descriptor.setValue(wr.getData(mMtu));
					}
					result = internalWriteDescriptor(descriptor);
					break;
				}
				case BEGIN_RELIABLE_WRITE: {
					result = internalBeginReliableWrite();
					// There is no callback for begin reliable write request.
					// Notify success and start next request immediately.
					if (result) {
						mRequest.notifySuccess(mBluetoothDevice);
						nextRequest(true);
						return;
					}
					break;
				}
				case EXECUTE_RELIABLE_WRITE: {
					result = internalExecuteReliableWrite();
					break;
				}
				case ABORT_RELIABLE_WRITE: {
					result = internalAbortReliableWrite();
					break;
				}
				case ENABLE_NOTIFICATIONS: {
					result = internalEnableNotifications(request.characteristic);
					break;
				}
				case ENABLE_INDICATIONS: {
					result = internalEnableIndications(request.characteristic);
					break;
				}
				case DISABLE_NOTIFICATIONS: {
					result = internalDisableNotifications(request.characteristic);
					break;
				}
				case DISABLE_INDICATIONS: {
					result = internalDisableIndications(request.characteristic);
					break;
				}
				case READ_BATTERY_LEVEL: {
					result = internalReadBatteryLevel();
					break;
				}
				case ENABLE_BATTERY_LEVEL_NOTIFICATIONS: {
					result = internalSetBatteryNotifications(true);
					break;
				}
				case DISABLE_BATTERY_LEVEL_NOTIFICATIONS: {
					result = internalSetBatteryNotifications(false);
					break;
				}
				case ENABLE_SERVICE_CHANGED_INDICATIONS: {
					result = ensureServiceChangedEnabled();
					break;
				}
				case REQUEST_MTU: {
					final MtuRequest mr = (MtuRequest) request;
					if (mMtu != mr.getRequiredMtu()
							&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
						result = internalRequestMtu(mr.getRequiredMtu());
					} else {
						result = mConnected;
						if (result) {
							mr.notifyMtuChanged(mBluetoothDevice, mMtu);
							mr.notifySuccess(mBluetoothDevice);
							nextRequest(true);
							return;
						}
					}
					break;
				}
				case REQUEST_CONNECTION_PRIORITY: {
					final ConnectionPriorityRequest cpr = (ConnectionPriorityRequest) request;
					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
						mConnectionPriorityOperationInProgress = true;
						result = internalRequestConnectionPriority(cpr.getRequiredPriority());
					} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
						result = internalRequestConnectionPriority(cpr.getRequiredPriority());
						// There is no callback for requestConnectionPriority(...) before Android Oreo.
						// Let's give it some time to finish as the request is an asynchronous operation.
						if (result) {
							final BluetoothDevice device = mBluetoothDevice;
							mHandler.postDelayed(() -> {
								cpr.notifySuccess(device);
								nextRequest(true);
							}, 100);
						}
					}
					break;
				}
				case SET_PREFERRED_PHY: {
					final PhyRequest pr = (PhyRequest) request;
					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
						result = internalSetPreferredPhy(pr.getPreferredTxPhy(),
								pr.getPreferredRxPhy(), pr.getPreferredPhyOptions());
					} else {
						result = mConnected;
						if (result) {
							pr.notifyLegacyPhy(mBluetoothDevice);
							pr.notifySuccess(mBluetoothDevice);
							nextRequest(true);
							return;
						}
					}
					break;
				}
				case READ_PHY: {
					final PhyRequest pr = (PhyRequest) request;
					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
						result = internalReadPhy();
					} else {
						result = mConnected;
						if (result) {
							pr.notifyLegacyPhy(mBluetoothDevice);
							pr.notifySuccess(mBluetoothDevice);
							nextRequest(true);
							return;
						}
					}
					break;
				}
				case READ_RSSI: {
					result = internalReadRssi();
					break;
				}
				case REFRESH_CACHE: {
					result = internalRefreshDeviceCache();
					if (result) {
						final BluetoothDevice device = mBluetoothDevice;
						mHandler.postDelayed(() -> {
							log(Log.INFO, "Cache refreshed");
							mRequest.notifySuccess(device);
							mRequest = null;
							if (mValueChangedRequest != null) {
								mValueChangedRequest.notifyFail(device, FailCallback.REASON_NULL_ATTRIBUTE);
								mValueChangedRequest = null;
							}
							cancelQueue();
							mInitQueue = null;
							if (mConnected) {
								// Invalidate all services and characteristics
								onDeviceDisconnected();
								// And discover services again
								log(Log.VERBOSE, "Discovering Services...");
								log(Log.DEBUG, "gatt.discoverServices()");
								mBluetoothGatt.discoverServices();
							}
						}, 200);
					}
					break;
				}
				case SLEEP: {
					final BluetoothDevice device = mBluetoothDevice;
					if (device != null) {
						final SleepRequest sr = (SleepRequest) request;
						log(Log.DEBUG, "sleep(" + sr.getDelay() + ")");
						mHandler.postDelayed(() -> {
							sr.notifySuccess(device);
							nextRequest(true);
						}, sr.getDelay());
						result = true;
					}
					break;
				}
				case WAIT_FOR_NOTIFICATION:
				case WAIT_FOR_INDICATION:
					// Those were handled before.
					break;
			}
			// The result may be false if given characteristic or descriptor were not found
			// on the device, or the feature is not supported on the Android.
			// In that case, proceed with next operation and ignore the one that failed.
			if (!result) {
				mRequest.notifyFail(mBluetoothDevice,
						mConnected ?
								FailCallback.REASON_NULL_ATTRIBUTE :
								BluetoothAdapter.getDefaultAdapter().isEnabled() ?
										FailCallback.REASON_DEVICE_DISCONNECTED :
										FailCallback.REASON_BLUETOOTH_DISABLED);
				mValueChangedRequest = null;
				mConnectionPriorityOperationInProgress = false;
				nextRequest(true);
			}
		}

		/**
		 * Returns true if this descriptor is from the Service Changed characteristic.
		 *
		 * @param descriptor the descriptor to be checked
		 * @return true if the descriptor belongs to the Service Changed characteristic
		 */
		private boolean isServiceChangedCCCD(@Nullable final BluetoothGattDescriptor descriptor) {
			return descriptor != null &&
					SERVICE_CHANGED_CHARACTERISTIC.equals(descriptor.getCharacteristic().getUuid());
		}

		/**
		 * Returns true if this is the Service Changed characteristic.
		 *
		 * @param characteristic the characteristic to be checked
		 * @return true if it is the Service Changed characteristic
		 */
		private boolean isServiceChangedCharacteristic(@Nullable final BluetoothGattCharacteristic characteristic) {
			return characteristic != null &&
					SERVICE_CHANGED_CHARACTERISTIC.equals(characteristic.getUuid());
		}

		/**
		 * Returns true if the characteristic is the Battery Level characteristic.
		 *
		 * @param characteristic the characteristic to be checked
		 * @return true if the characteristic is the Battery Level characteristic.
		 */
		@Deprecated
		private boolean isBatteryLevelCharacteristic(@Nullable final BluetoothGattCharacteristic characteristic) {
			return characteristic != null &&
					BATTERY_LEVEL_CHARACTERISTIC.equals(characteristic.getUuid());
		}

		/**
		 * Returns true if this descriptor is a Client Characteristic Configuration descriptor (CCCD).
		 *
		 * @param descriptor the descriptor to be checked
		 * @return true if the descriptor is a CCCD
		 */
		private boolean isCCCD(final BluetoothGattDescriptor descriptor) {
			return descriptor != null &&
					CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID.equals(descriptor.getUuid());
		}
	}

	protected static final int PAIRING_VARIANT_PIN = 0;
	protected static final int PAIRING_VARIANT_PASSKEY = 1;
	protected static final int PAIRING_VARIANT_PASSKEY_CONFIRMATION = 2;
	protected static final int PAIRING_VARIANT_CONSENT = 3;
	protected static final int PAIRING_VARIANT_DISPLAY_PASSKEY = 4;
	protected static final int PAIRING_VARIANT_DISPLAY_PIN = 5;
	protected static final int PAIRING_VARIANT_OOB_CONSENT = 6;

	@Retention(RetentionPolicy.SOURCE)
	@IntDef(value = {
			PAIRING_VARIANT_PIN,
			PAIRING_VARIANT_PASSKEY,
			PAIRING_VARIANT_PASSKEY_CONFIRMATION,
			PAIRING_VARIANT_CONSENT,
			PAIRING_VARIANT_DISPLAY_PASSKEY,
			PAIRING_VARIANT_DISPLAY_PIN,
			PAIRING_VARIANT_OOB_CONSENT
	})
	public @interface PairingVariant {}

	protected String pairingVariantToString(@PairingVariant final int variant) {
		switch (variant) {
			case PAIRING_VARIANT_PIN:
				return "PAIRING_VARIANT_PIN";
			case PAIRING_VARIANT_PASSKEY:
				return "PAIRING_VARIANT_PASSKEY";
			case PAIRING_VARIANT_PASSKEY_CONFIRMATION:
				return "PAIRING_VARIANT_PASSKEY_CONFIRMATION";
			case PAIRING_VARIANT_CONSENT:
				return "PAIRING_VARIANT_CONSENT";
			case PAIRING_VARIANT_DISPLAY_PASSKEY:
				return "PAIRING_VARIANT_DISPLAY_PASSKEY";
			case PAIRING_VARIANT_DISPLAY_PIN:
				return "PAIRING_VARIANT_DISPLAY_PIN";
			case PAIRING_VARIANT_OOB_CONSENT:
				return "PAIRING_VARIANT_OOB_CONSENT";
			default:
				return "UNKNOWN";
		}
	}

	protected String bondStateToString(final int state) {
		switch (state) {
			case BluetoothDevice.BOND_NONE:
				return "BOND_NONE";
			case BluetoothDevice.BOND_BONDING:
				return "BOND_BONDING";
			case BluetoothDevice.BOND_BONDED:
				return "BOND_BONDED";
			default:
				return "UNKNOWN";
		}
	}

	protected String writeTypeToString(final int type) {
		switch (type) {
			case BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT:
				return "WRITE REQUEST";
			case BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE:
				return "WRITE COMMAND";
			case BluetoothGattCharacteristic.WRITE_TYPE_SIGNED:
				return "WRITE SIGNED";
			default:
				return "UNKNOWN: " + type;
		}
	}

	/**
	 * Converts the connection state to String value.
	 *
	 * @param state the connection state
	 * @return state as String
	 */
	protected String stateToString(final int state) {
		switch (state) {
			case BluetoothProfile.STATE_CONNECTED:
				return "CONNECTED";
			case BluetoothProfile.STATE_CONNECTING:
				return "CONNECTING";
			case BluetoothProfile.STATE_DISCONNECTING:
				return "DISCONNECTING";
			default:
				return "DISCONNECTED";
		}
	}

	/**
	 * Converts the PHY number to String value.
	 *
	 * @param phy phy value
	 * @return phy as String
	 */
	@RequiresApi(value = Build.VERSION_CODES.O)
	private String phyToString(final int phy) {
		switch (phy) {
			case BluetoothDevice.PHY_LE_1M:
				return "LE 1M";
			case BluetoothDevice.PHY_LE_2M:
				return "LE 2M";
			case BluetoothDevice.PHY_LE_CODED:
				return "LE Coded";
			default:
				return "UNKNOWN (" + phy + ")";
		}
	}

	@RequiresApi(value = Build.VERSION_CODES.O)
	private String phyMaskToString(final int mask) {
		switch (mask) {
			case BluetoothDevice.PHY_LE_1M_MASK:
				return "LE 1M";
			case BluetoothDevice.PHY_LE_2M_MASK:
				return "LE 2M";
			case BluetoothDevice.PHY_LE_CODED_MASK:
				return "LE Coded";
			case BluetoothDevice.PHY_LE_1M_MASK | BluetoothDevice.PHY_LE_2M_MASK:
				return "LE 1M or LE 2M";
			case BluetoothDevice.PHY_LE_1M_MASK | BluetoothDevice.PHY_LE_CODED_MASK:
				return "LE 1M or LE Coded";
			case BluetoothDevice.PHY_LE_2M_MASK | BluetoothDevice.PHY_LE_CODED_MASK:
				return "LE 2M or LE Coded";
			case BluetoothDevice.PHY_LE_1M_MASK | BluetoothDevice.PHY_LE_2M_MASK
					| BluetoothDevice.PHY_LE_CODED_MASK:
				return "LE 1M, LE 2M or LE Coded";
			default:
				return "UNKNOWN (" + mask + ")";
		}
	}

	@RequiresApi(value = Build.VERSION_CODES.O)
	private String phyCodedOptionToString(final int option) {
		switch (option) {
			case BluetoothDevice.PHY_OPTION_NO_PREFERRED:
				return "No preferred";
			case BluetoothDevice.PHY_OPTION_S2:
				return "S2";
			case BluetoothDevice.PHY_OPTION_S8:
				return "S8";
			default:
				return "UNKNOWN (" + option + ")";
		}
	}
}
